본문 바로가기
개발자 꿈나무의 하루/01_Boot Camp

(네이버클라우드 부트캠프) 22일차 - Java프로그래밍 기초(String, Wrapper),실습프로젝트(LinkedList)

by kk_naks 2024. 6. 25.

1. 문자열 객체

  1.1  Heap과 String pool의 관계 

     - new String()

- new String()으로 생성한 객체는 Heap영역에 보관된다. 
String s1 = new String("Hello");
String s2 = new String("Hello");

-> s1과 s2의 인스턴스는 다르다

 

     - String pool

- ""으로 할당된 문자열은 String pool에 생성된다.
- 다른 변수에 String pool에 있는 문자열을 할당하면 동일한 인스턴스를 가진다.
String x1 = "Hello"; // 새 String 인스턴스의 주소를 리턴한다.
String x2 = "Hello"; // 기존의 String 인스턴스 주소를 리턴한다.

-> 두객체의 인스턴스는 같다.

 

 

     - 차이점

- 두 방식의 차이점을 Heap에 인스턴스를 생성하냐, String Pool에 인스턴스를 생성하냐의 차이이다.
- 근본적으로 두 방식 모두 String 객체의 인스턴스이다. 
String s1 = new String("Hello"); // Heap 영역에 String 인스턴스를 만든다.
String s2 = "Hello";  // String Pool 영역에 String 인스턴스를 만든다.

System.out.println(s1 == s2);  //false
System.out.println(s1 instanceof String); //true
System.out.println(s2 instanceof String); //true

 

     - String.intern()

- intern() 메서드 : String 객체의 문자열과 동일한 문자열을 갖는 String객체를 String pool에서 찾는다.
- String pool에 있으면, 해당 주소를 리턴한다. 
- String pool에 없으면, String pool에 해당 객체를 만들고 그 주소를 리턴한다. 
Case 1.
String s1 = new String("Hello");  // Heap 영역에 String 인스턴스를 생성한다.
String s2 = s1.intern(); //String pool에 Hello를 생성
String s3 = "Hello";  // 해당 문자열을 가진 String 객체를 String Pool에서 찾는다. 

System.out.println(s1 == s2); //false
System.out.println(s2 == s3); //true

Case 2.
String s1 = new String("Hello");  // Heap 영역에 String 인스턴스를 생성한다.
String s2 = "Hello";  //String pool에 Hello를 생성
String s3 = s1.intern(); //해당 문자열을 가진 String 객체를 String Pool에서 찾는다. 

System.out.println(s1 == s2); //false
System.out.println(s2 == s3); //true

 

  1.2  String.toString() 다형성

- 다음과 같이 코드가 주어질 때, 형변환을 String 객체로 대입할 수 있다. 
Object obj = new String("Hello"); // 인스턴스 주소가 100이라 가정하자;
String x1 = (String) obj; // x1 <--- 100
System.out.println(obj == x1);
- Object 클래스에 선언된 멤버여도 실제 obj객체가 가리키는 클래스부터 찾아 올라간다. 
- obj가 String으로 인스턴스 주소를 받았기 때문에 String 클래스의 toString()을 먼저 찾는다. 
- String의 toString()이 오버라이딩 되었기 때문에 Hello를 리턴한다.
String x2 = obj.toString(); // x2 <---- 100
System.out.println(x2); // Hello
System.out.println(x1 == x2); // true
- 원래 타입으로 형변환을 해야 해당 타입의 클래스의 메소드를 호출 할 수 있다. 

 

  1.3  mutable 객체와 immutable 객체

- String 객체는 immutable로 한번 정해지면 데이터를 변경할 수 없다.
String s1 = new String("Hello");

// String 클래스의 메서드는 원본 인스턴스의 데이터를 변경하지 않는다. 
// 다만 새로 String 객체를 만들 뿐이다.
String s2 = s1.replace('l', 'x');
System.out.println(s1 == s2); // false
System.out.printf("%s : %s\n", s1, s2); // 원본은 바뀌지 않는다.

String s3 = s1.concat(", world!");
System.out.println(s1 == s3); // false
System.out.printf("%s : %s\n", s1, s3); // 원본은 바뀌지 않는다.
- StringBuffer은 mutable 객체이다.
StringBuffer buf = new StringBuffer("Hello");
System.out.println(buf); // Hello 
System.out.println(buf.hashCode()); //498931366

buf.replace(2, 4, "xxxx");// 원본을 바꾼다.
System.out.println(buf); //Hexxxxo
System.out.println(buf.hashCode()); //498931366
- StringBuilder은 mutable객체이다.
StringBuilder strBuilder = new StringBuilder("Hello");
System.out.println(strBuilder); // Hello
System.out.println(strBuilder.hashCode()); // 498931366


strBuilder.replace(2, 4, "xxxx");// 원본을 바꾼다.
System.out.println(strBuilder); // Hexxxxo
System.out.println(strBuilder.hashCode());// 498931366

  1.4  StringBuffer와 StringBuilder

- 두 클래스의 차이는 동시성이다.
- Buffer은 멀티스레드 환경에서 순서를 정해서 인스턴스에 접근한다.
- Builder는 비동기화로 단일 스레드 환경에서 순차적으로 인스턴스에 접근한다. 

 

  1.5  String복사하기

- String을 복사하는 메서드는 Arrays.copyOfRange(args[], start,end)
- 이때 범위는 (start,end]의 범위를 가진다.
String[] arr = {"101", "제목", "내용", "4", "2021-2-2"};

// 배열에서 특정 범위의 항목을 복사하기
String[] arr2 = Arrays.copyOfRange(arr, 2, 4);
for (String s : arr2) {
  System.out.println(s);

 

  1.6  배열과 String

String s1 = new String();
System.out.println(s1); // 빈 문자열 출력

String s2 = new String("Hello"); // 문자열 리터럴로 String 인스턴스 생성
System.out.printf("s2=%s\n", s2);

char[] chars = {'H', 'e', 'l', 'l', 'o'};
String s3 = new String(chars); // char 배열로 String 인스턴스 생성
System.out.printf("s3=%s\n", s3);

byte[] bytes =
    {(byte) 0xb0, (byte) 0xa1, (byte) 0xb0, (byte) 0xa2, 0x30, 0x31, 0x32, 0x41, 0x42, 0x43};
String s4 = new String(bytes);
System.out.printf("s4=%s\n", s4);
// 한글이 깨진다. 이유?

String s5 = new String(bytes, "euc-kr");
System.out.printf("s5=%s\n", s5);

byte[] bytes2 =
    {(byte) 0xac, (byte) 0x00, (byte) 0xac, (byte) 0x01, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63};
String s6 = new String(bytes2, "utf-16");
System.out.printf("s6=%s\n", s6);

byte[] bytes3 = {(byte) 0xea, (byte) 0xb0, (byte) 0x80, (byte) 0xea, (byte) 0xb0, (byte) 0x81,
    0x61, 0x62, 0x63};
String s7 = new String(bytes3, "utf-8");
System.out.printf("s7=%s\n", s7);

 

2. Wrapper class

- primitive type은 객체가 아니므로 인스턴스 변수를 활용할수 없다.
- 이를 해결하기 위해 wrapper를 활용
Byte b2 = Byte.valueOf((byte)100);
Short s2 = Short.valueOf((short)20000);
Integer i2 = Integer.valueOf(3000000);
Long l2 = Long.valueOf(60000000000L);
Float f2 = Float.valueOf(3.14f);
Double d2 = Double.valueOf(3.14159);
Boolean bool2 = Boolean.valueOf(true);
Character c2 = Character.valueOf((char)0x41);

  2.1  wrapper class 활용

- wrapper 클래스를 사용하지 않으면 primitive type에 대해 각각 메서드를 만들어야한다. 
long l = 100L;
double d = 3.14;
boolean bool = true;

m(l);
m(d);
m(bool);

static void m(long value) { // byte, short, int, long, char
System.out.printf("long value=%s\n", value);
}
static void m(double value) {// float, double
System.out.printf("double value=%s\n", value);
}
static void m(boolean value) {// boolean
System.out.printf("boolean value=%s\n", value);
}
- wrapper 클래스를 사용하여 객체로 만들면 Object를 활용하여 객체를 받는 메소드로 만들 수 있다.
Long obj1 = Long.valueOf(l);
Double obj2 = Double.valueOf(d);
Boolean obj3 = Boolean.valueOf(bool);

m(obj1);
m(obj2);
m(obj3);

static void m(Object value) { // 모든 객체를 받을 수 있다.
System.out.printf("wrapper value=%s\n", value);
}

  2.2 auto-boxing

- 컴파일 시 Wrapper 클래스가 적용된 객체에 대해서는 자동으로 객체로 변환한다.
int i1 = 100;
Integer obj1 = Integer.valueOf(i1);
Integer obj2 = 100; // ==> Integer.valueOf(100)

  2.3 auto-unboxing

- 컴파일 시 오토 언박싱도 수행한다. 
Integer obj = Integer.valueOf(100);
int i2 = obj.intvalue()
int i3 = obj;  // ==> obj.intvalue()

 

3. 실습프로젝트

  3.1 배열의 장단점

장점 단점
1. 고정된 크기
 배열의 크기를 미리 정의하면, 메모리 관리가 용이
 필요한 크기만큼의 메모리를 할당
1. 고정된 크기
선언 시점에 크기를 고정하여 컴파일 후 크기 불가
 데이터의 양을 정확히 예측하기 어렵거나, 가변적인 경우에 부적합
2. 빠른 데이터 접근
 배열의 인덱스를 사용하여 상수 시간 내에 O(1) 접근
 특정 위치의 데이터를 빠르게 읽고 쓸 수 있음
2. 메모리 낭비
 필요한 양보다 큰 배열을 선언하면 메모리가 낭비
 반대로, 배열이 작으면 데이터가 초과
3. 연속적인 메모리 할당
 배열은 메모리 상에 연속적으로 저장되므로, 캐시 효율성이 높음
3. 삽입 및 삭제의 비효율성
 배열 중간에 요소를 삽입하거나 삭제할 때, 다른 요소를 이동
시간 복잡도가 O(n)인 작업으로, 대용량 데이터에서 비효율적
4. 간단한 구조
 배열은 데이터 구조가 간단하고 사용법이 직관적

4. 타입제한
 자바의 배열은 단일 타입의 데이터만 저장
 다양한 타입의 데이터를 저장해야 할 때는 불편

  3.2 가변배열 만들기

- grow() 메서드로 새로운 배열 만들기
private void grow() {
    int oldSize = list.length;
    int newSize = oldSize + (oldSize >> 1);
    Object[] arr = new Object[newSize];
    for (int i = 0; i < oldSize; i++) {
      arr[i] = list[i];
    }
    list = arr;
}
public void add(Object obj) {
    if (size == list.length) {
      int oldSize = list.length;
      int newSize = oldSize + (oldSize >> 1);
      grow();
    }
    list[size++] = obj;
}
- Arrays메서드 활용
public void add(Object obj) {
    if (size == list.length) {
      int oldSize = list.length;
      int newSize = oldSize + (oldSize >> 1);
      list = Arrays.copyOf(list,newSize);
      grow();
    }
    list[size++] = obj;
}

 

  3.3 LinkedList 만들기

- LinkedList의 구조는 Node에 다음 Node의 레퍼런스를 가지게 함으로서 객체간에 연결을 하는 구조이다.

//노드의 기본구조
public class Node {
  Object value;
  Node next;

  public Node(Object value) {
    this.value = value;
  }
}
- LinkedList 연결하는 기본원리는 새로 생성된 주소 값을 받는 것이다.

public void append(Object value) {
    size++;
    Node newNode = new Node(value);
    //첫 노드의 경우 first와 last의 필드값이 null이다. 
    if (first == null) {
      first = last = newNode;
      return;
    }
    // 이후 노드는 
    last.next = newNode; //추가된 노드의 주소를 넘긴다
    last = newNode; //추가된 노드의 객체의 주소를 넘긴다
}
- LinkedList의 조회는 다음 주소값을 넘어가고 인덱스 일치 여부를 판단하는 것이다.

public Object getValue(int index) {
    if (index < 0 || index >= size) {
      return null;
    }
    Node cursor = first;
    int currentIndex = 0;

    while (cursor != null) {
      if (currentIndex == index) {
        return cursor.value;
      }
      cursor = cursor.next;
      currentIndex++;
    }
    return null;
  }
- LinkedList의 삭제는 다음의 경우를 고려해야한다.
- index = 0 인경우 : first를 first.next로 넘겨주고 단일 Link인지 확인
- 중간에 위치한 경우 : index-1 지점 까지 커서를 이동 후 cusor.next = cusor.next.next와 연결한다. 
- 끝지점에 위치한 경우 : last의 한칸 땡기기

 public Object delete(int index) {
    if (index < 0 || index >= size) {
      return null;
    }

    size--;
    Node deletedNode = null;

    Node cursor = first;
    int currentIndex = 0;

    if (index == 0) {
      deletedNode = first;
      first = first.next;
      if (first == null) {
        last = null;
      }
      return deletedNode.value;
    }

    while (cursor != null) {
      if (currentIndex == index - 1) {
        break;
      }
      cursor = cursor.next;
      currentIndex++;
    }
    deletedNode = cursor.next;
    cursor.next = cursor.next.next;
    if (cursor.next == null) {
      last = cursor;
    }
    return deletedNode.value;
  }