포스트

Day22 Java프로그래밍 기초(String, Wrapper),실습프로젝트(LinkedList)

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 객체로 대입할 수 있다.
1
2
3
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로 한번 정해지면 데이터를 변경할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
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]의 범위를 가진다.
1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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를 활용
1
2
3
4
5
6
7
8
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에 대해 각각 메서드를 만들어야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 클래스가 적용된 객체에 대해서는 자동으로 객체로 변환한다.
1
2
3
int i1 = 100;
Integer obj1 = Integer.valueOf(i1);
Integer obj2 = 100; // ==> Integer.valueOf(100)

2.3 auto-unboxing

  • 컴파일 시 오토 언박싱도 수행한다.
1
2
3
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() 메서드로 새로운 배열 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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의 레퍼런스를 가지게 함으로서 객체간에 연결을 하는 구조이다.



1
2
3
4
5
6
7
8
9
//노드의 기본구조
public class Node {
  Object value;
  Node next;

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



1
2
3
4
5
6
7
8
9
10
11
12
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의 조회는 다음 주소값을 넘어가고 인덱스 일치 여부를 판단하는 것이다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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의 한칸 땡기기



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 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;
  }
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.