본문 바로가기

국비 교육

2020.8.14일자 수업 : String, Wrapper, ArrayList 구현 실습

String

git/ eomcs-java-basic/ src/ main/ java com.eomcs.corelib.ex02.Exam0110~180.java

 

문자열 객체 생성 방법

1. String str =  new String("abc");

heap에 String 인스턴스를 생성한다.

내용의 동일 여부를 확인하지 않고 생성할 때마다 새로운 인스턴스를 생성한다.

 

2. String str = "abc";

heap이 아니라 string constant pool (상수풀) 메모리 영역에 String 인스턴스를 생성한다.

내용물이 같으면 기존 인스턴스의 주소를 리턴한다.

즉 메모리 절약을 위해 중복 데이터를 갖는 인스턴스를 생성하지 않는다.

그리고 만들어진 인스턴스는 JVM이 끝날 때까지 메모리에 유지된다.

 

3. intern() - 인스턴스 메서드

지정된 String 객체를 상수풀에서 찾는다.

있으면 그 String 객체의 주소를 리턴한다.

없으면 상수풀에 String 객체를 생성한 후 그 주소를 리턴한다.

 

equals(), hashCode(), toString()

오브젝트 클래스가 가진 이 세 메서드는 String 클래스에서 오버라이딩되었다.

 

1. equals()

오브젝트 클래스의 equals()는 인스턴스의 주소가 같은 지 비교하지만, String 클래스에서 오버라이딩된 equals()는 문자열의 내용이 같은 지를 비교한다.

String s1 = new String("Hello");
String s2 = new String("Hello");

// 두 String 인스턴스는 분명히 서로 다르다.
System.out.println(s1 == s2);

// 두 인스턴스가 갖고 있는 문자열이 같은지를 비교하고 싶다면,
System.out.println(s1.equals(s2));

+) equalsIgnore() : String 클래스에서 추가된 메서드로서 대소문자 구분 없이 문자열의 내용이 같은 지 비교한다.

 

2. hashCode()

오브젝트 클래스의 hashCode()는 인스턴스마다 다른 값이 리턴되지만, String 클래스에서 오버라이딩된 hashCode()는 문자열이 같으면 같은 값을 리턴한다.

String s1 = new String("Hello");
String s2 = new String("Hello");

// Object의 hashCode()는 인스턴스 마다 다르다.
System.out.println(s1.hashCode() == s2.hashCode()); // true

 

* StringBuffer : equals()와 hashCode()가 오버라이딩이 되어있지 않기 때문에 인스턴스 주소가 다르면 모두 다르게 나온다.

StringBuffer 인스턴스의 내용을 비교하고 싶다면 오버라이딩된 toString()을 통해서 String으로 변환시켜 비교가 가능하다.

StringBuffer b1 = new StringBuffer("Hello");
StringBuffer b2 = new StringBuffer("Hello");

// StringBuffer 에 들어 있는 문자열을 비교하려면?
// - StringBuffer에서 String을 꺼내 비교하라!

String s1 = b1.toString();
String s2 = b2.toString();
System.out.println(s1.equals(s2));
    
System.out.println(b1.toString().equals(b2.toString()));

 

3. toString()

오브젝트 클래스의 toString()은 "클래스명@해시값"을 리턴하지만 String 클래스에서 오버라이딩된 toString()은 this 주소를 그대로 리턴한다. 

public class Exam0140 {
  public static void main(String[] args) {
    String s1 = new String("Hello");

    String s2 = s1.toString();
    // Object.toString()은 "클래스명@해시값" 을 리턴한다.
    // String은 상속 받은 toString()을 오버라이딩 했다.
    // => this 주소를 그대로 리턴한다.
    System.out.println(s1 == s2); // true

    System.out.println(s1);
  }
}

println(s1.toString())과 println(s1)은 같은 문장이다. 또한 println() 메서드는 String 클래스의 인스턴스 주소를 파라미터 값으로 받으면 해당 주소가 가리키는 인스턴스에 들어있는 문자열을 그대로 출력한다.

 

* 다형적 변수와 메서드 : 오브젝트 타입의 레퍼런스 변수에 String 타입의 인스턴스를 넣는 경우에는 오브젝트 클래스에 있는 메서드만 호출할 수 있다. 그런데 메서드를 호출하게 되면 해당 인스턴스의 타입이 되는 클래스부터 메서드를 찾아올라간다. 따라서 toString() 메서드는 오브젝트 클래스의 메서드이므로 호출이 가능하며 호출하면 String 클래스에서 오버라이딩된 메서드가 실행된다. 따라서 리턴되는 값은 '클래스@해시코드' 가 아니라, 인스턴스의 주소이다.

package com.eomcs.corelib.ex02;

public class Exam0141 {
  public static void main(String[] args) {

    Object obj = new String("Hello"); // 인스턴스 주소가 100이라 가정하자;

    String x1 = (String) obj; // x1 <= 100

    String x2 = obj.toString(); // x2 <= 100

    System.out.println(x1 == x2);

 

mutable vs immutable

  • immutable : 한번 객체에 값을 담으면 변경이 불가능하다. 변경할 때마다 새 인스턴스가 계속 만들어지고, 또 한편으로 가비지가 늘어나는 점이 단점이다. String 클래스가 immutable 객체에 속한다.
String s1 = new String("Hello");

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

String s3 = s1.concat(", world!");
System.out.printf("%s : %s\n", s1, s3); // 원본은 바뀌지 않는다.
  • mutable : 객체의 원본을 편집할 수 있는 타입의 객체이다. 문자열의 원본을 바꾸고 싶다면 mutable 객체인 StringBuffer을 사용하면 된다.
StringBuffer buf = new StringBuffer("Hello");
System.out.println(buf);

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

 

String 의 스태틱 메서드

  • String.format(String, Object...) 
  • String.valueOf() : primitive 데이터 값을 문자열로 변환

다양한 생성자 활용

String 인스턴스를 생성하면 내부적으로 문자의 코드 값을 저장할 char 배열 혹은 byte 배열을 생성한다. 버전 1.8까지는 char 배열안에 UTF-16 형태로 저장했었다. 그러나 버전 1.9부터는 메모리를 줄이기 위해서 byte 배열을 생성하여 영어는 1바이트만으로 관리를 할 수 있도록 1바이트 단위로 바꾸고 그 이상을 넘어가게 될 경우에 2-3개 메모리를 사용하는 방식으로 바꿨다.

 

1. new String()

파라미터로 아무것도 주지 않을 시에는 빈 배열이 만들어지고 이를 출력하면 빈 문자열이 나온다.

 

2. new String(String)

문자열 리터럴로 String 인스턴스를 생성할 수 있다.

 

3. new String(char[])

캐릭터 배열로 String 인스턴스를 생성할 수 있다.

 

4. new String(byte[], String)

해당 바이트 배열이 어떤 문자 집합에 의해 짜진 문자 코드인지 알려주지 않으면 해당 코드를 ISO-8859-1(서유럽언어 확장 코드(1바이트))로 간주하고 그에 따라 해당 코드를 UTF-16으로 바꿔 저장한다. 따라서 ISO-8859-1 이외에 다른 문자 집합으로 짠 문자코드라면 반드시 해당 문자 집합에 관한 정보를 두 번째 파라미터로 넘겨줘야한다.

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);

 


Wrapper

git/ eomcs-java-basic/ src/ main/ java com.eomcs.corelib.ex02.Exam0210~0231.java

자바는 primitive data type의 값을 다룰 때 기본 연산자 외에 좀 더 다양한 방법으로 다루기 위해 primitive data type에 대응하는 클래스를 제공한다. 이렇게 primitive data type에 대응하여 만든 클래스를 primitive data를 포장하는 객체라고 해서 "랩퍼(wrapper) 클래스"라 부른다. primitive data type의 값을 객체로 전달하고 싶을 때는 wrapper 클래스의 인스턴스를 생성하면 된다.

 

new 연산자와 valueOf() 

new 연산자는 heap에 인스턴스를 생성하고 값의 동일여부와 상관없이 새 인스턴스를 생성하나, valueOf는  -128~127까지의 수는 상수 풀에 생성하여 값이 동일한 인스턴스를 중복 생성하지 않는다. 이렇게 상수 풀에 생성된 인스턴스는 JVM이 종료될때까지 사라지지 않는다. 반면, 그 범위 밖의 수는 오히려 수가 차지하는 메모리가 크기가 너무 커져서 heap에 생성하여 사용되지 않는 경우 가비지가 될 수 있도록 한다. new 연산자는 모든 객체를 heap에 저장하기 때문에 가능한한 쓰지 않는 것이 좋다. 

 

== 와 equals()

new 연산자를 통해서 -128부터 127까지 사이의 수 중에 한 값을 두 번 생성하여 ==로 비교하면 true 가 나온다. 하나의 인스턴스만이 상수 풀에 생성되었기 때문이다. 반대로 범위 밖의 수를 두 번 생성하면 그에 따라 heap에 두 개의 다른 인스턴스가 생성되므로 두 객체를 == 로 비교했을 시 false 라는 결론이 나온다. 

Integer i3 = Integer.valueOf(127);
Integer i4 = Integer.valueOf(127);
System.out.println(i3 == i4); // true

Integer x = Integer.valueOf(-128);
Integer y = Integer.valueOf(-128);
System.out.println(x == y); // true

따라서 매번 -128과 127 범위를 고려하여 연산자를 쓰기 힘들기 때문에 그냥 equals()를 사용하는 편이 좋다.

 

auto-boxing / auto-unboxing (JDK1.5~)

더보기

primitive data type -> wrapper class 변환방법 : Integer.valueOf() 스태틱 메서드 사용
Integer obj1 = Integer.valueOf(100);

 

wrapper class -> primitive data type 변환 방법: intValue() 인스턴스 메서드 사용

Integer obj2 = Integer.valueOf(200);

int i2 = obj2.intValue();

1. auto-boxing

primitive data 타입의 값을 그대로 Wrapper 클래스 변수에 할당하면 내부적으로 우항에 Integer.valueOf() 메서드가 추가된다. 따라서 int 값을 그대로 Wrapper 변수에 넣는 것이 아니라 Wrapper 객체가 생성되어 그 주소를 저장하는 것이다. 이를 오토 박싱(auto-boxing)이라고 부른다. 

Integer obj = 100; // ==> Integer.valueOf(100)

 

2. auto-unboxing

Wrapper 객체의 값을 primitive data 타입 변수에 할당하면 내부적으로 우항에 intValue() 메서드가 추가된다. 따라서 인스턴스 주소가 변수에 저장되는 것이 아니라 인스턴스의 있는 값을 꺼내서 변수에 저장하는 것이다. 이를 오토 언박싱(auto-unboxing)이라고 부른다. 

Integer obj = Integer.valueOf(300);
int i = obj; // ==> obj.intValue()

 

이 기능으로 인해, Object 객체 변수에는 Wrapper 클래스든, primitive data 타입이든 상관없이 편리하게 값을 저장할 수 있게 되었다.

Object obj;
obj = Integer.valueOf(200); 
obj = new String("aaa") 
obj = 100; 

이에 따라 메서드를 정의할 때에도 파라미터 타입을 클래스와 primitive data type로 구분하여 여러 개 만들 필요가 없어졌다.

static void print(int i) {
    System.out.print("정수: ");
    System.out.println(i);
  }

  static void print(Member m) {
    System.out.print("회원: ");
    System.out.println(m);
  }

  // auto-boxing/auto-unboxing 기능이 제공되기 때문에
  // 다음과 같이 primitive type의 값과 객체를 구분하지 않고
  // Object 파라미터를 사용하여 처리할 수 있다.
  //
  static void printObject(Object obj) {
    System.out.println(obj);
  }

 

주의할 것 !

auto-boxing 으로 객체를 생성하면 new 생성자가 아닌 valueOf() 메서드로 인스턴스가 생성되기 때문에 해당 인스턴스가 heap이 아닌 상수 풀에 만들어짐을 알고 있어야한다. 따라서 auto-boxing으로 생성된 -128~127 사이의 같은 값의 인스턴스들은 중복 생성되지 않기 때문에 서로를 == 연산자로 비교해도 같다고 나온다. 그러나 -128~127 범위 밖의 값은 heap에 생성되기 때문에 == 연산자로 비교하면 같은 값이라도 다르다고 나온다. 따라서 == 보다는 equals() 메서드를 사용하자.

 


Date

git/ eomcs-java-basic/ src/ main/ java com.eomcs.corelib.ex02.Exam0310.java

생성자

1. Date() 기본 생성자

현재시간을 날짜 형태로 저장한다.

 

2. Date(long)

1970-01-01 00:00:00 부터 경과된 밀리초를 파라미터 값으로 주면 시간을 계산하여 날짜 형태로 저장한다.

System.currentTimeMillis() 메서드는 1970-01-01 00:00:00 기준으로 지금까지 지난 밀리초를 계산하여 long 값으로 리턴하므로 이를 이용하면 지금의 시간을 날짜 형태로 저장할 수 있다.

 

3. Date(int, int, int)  deprecated

1900년 기준으로 지난 년, 월, 일의 값을 파라미터 값으로 주면 시간을 계산하여 날짜 형태로 저장한다.

 

4. java.sql.Date()

유틸 패키지의 Date 생성자와 마찬가지로 1970-01-01 00:00:00 기준으로 경과된 시간을 long값으로 주면 날짜와 시간을 계산하여 저장한다.

 

5. java.sql.Date(String)

yyyy-[m]m-[d]d 형태의 문자열을 주면 이를 날짜 형태로 변환하여 저장한다.

 

Calendar

git/ eomcs-java-basic/ src/ main/ java com.eomcs.corelib.ex02.Exam0410.java

 

Calendar 의 생성자는 접근 권한이 없어 호출 불가능하다. 따라서 getInstance()라는 별도의 스태틱 메서드를 통해 간접적으로 인스턴스를 생성하는 수 밖에 없다. 이는 Calendar의 인스턴스 생성 과정이 복잡하기 때문에 의도적으로 getInstance()를 통해서만 인스턴스를 생성할 수 있도록 유도한 것이다. 이러한 객체 생성 디자인 패턴을 팩토리 메서드라고 부른다.

 

더보기

객체 생성 디자인 패턴 중 일부 소개

1) 팩토리 메서드(factory method)

- GoF(Gang of Four)의 23가지 디자인 패턴(design pattern) 중 하나이다.

- 인스턴스를 생성해주는 메서드이다.

- 인스턴스 생성 과정이 복잡할 경우에 인스턴스를 생성해주는 메서드를 미리 정의해 둔다.

- 그래서 인스턴스가 필요할 때 마다 메서드를 호출하여 인스턴스를 리턴 받는다.

 

2) 싱글톤(singleton)

- GoF(Gang of Four)의 23가지 디자인 패턴(design pattern) 중 하나이다.

- 인스턴스를 한 개만 생성하도록 제한할 때 사용한다.

- 생성자를 private으로 처리하여 직접 인스턴스를 생성하지 못하도록 만든다.

- 메서드를 통해 인스턴스를 생성하도록 유도한다.

 


실습 - ArrayList  구현하기

git/ bitcamp-20200713/ bitcamp-java-basic/ src/ main/ java com.eomcs.corelib.ex03.MyArrayList01~13.java

 

1단계 : Object 배열을 만들고 유동적인 배열 크기를 구현하기 위해 배열 크기를  size 변수로 선언한다. 그리고 add(Object), add(int, Object), get(int), set(int, Object), remove(int)  메서드를 구현한다. 멤버들을 모두 스태틱 멤버로 선언한다.

package com.eomcs.corelib.ex03;

public class MyArrayList {
  static Object[] list = new Object[5];
  static int size;
  
  static void add(Object obj) {
    list[size] = obj;
    size++;
  }
  
  static void add(int index, Object obj) {
    for (int i = size - 1; i >= index; i--) {
      list[i+1] = list[i];
    }
    list[index] = obj;
    size++;
  }
  
  static Object get(int index) {
    return list[index];
  }
  
  static Object set(int index, Object obj) {
    Object old = list[index];
    list[index] = obj;
    return old;
  }
  
  static Object remove(int index) {
    Object old = list[index];
    for (int i = index; i < size - 1; i++) {
      list[index] = list[index + 1];
    }
    size--;
    return old;
  }
}

2단계 : Test를 병행하며 다양한 경우에 대한 유효성을 검사하고 이에 맞게 기존 코드를 변경한다. 

package com.eomcs.corelib.ex03;

public class MyArrayList {
  static Object[] list = new Object[5];
  static int size;
  
  static void add(Object obj) {
    // 사이즈가 배열보다 커지려할 때, 배열 크기 늘리기
    if (size == list.length) {
    // grow() 메서드 사용
      grow();
    }
    list[size] = obj;
    size++;
  }
  
  static void add(int index, Object obj) {
    // 유효한 인덱스 범위 제한, 에러 띄우기 
    if (index < 0 || index > size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    // 사이즈가 배열보다 커지려할 때, 배열 크기 늘리기
    if (size == list.length) {
      grow();
    }
    for (int i = size - 1; i >= index; i--) {
      list[i+1] = list[i];
    }
    list[index] = obj;
    size++;
  }
 
  static Object get(int index) {
    // 유효한 인덱스 범위 제한, 에러 띄우기
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    return list[index];
  }
  
  static Object set(int index, Object obj) {
    // 유효한 인덱스 범위 제한, 에러 띄우기
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    Object old = list[index];
    list[index] = obj;
    return old;
  }
  
  static Object remove(int index) {
    Object old = list[index];
    // 유요한 인덱스 범위 제한, 에러 띄우기
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    for (int i = index; i < size - 1; i++) {
      list[index] = list[index + 1];
    }
    size--;
    // 지우기 전 마지막 인덱스 null 처리
    list[size] = null;
    return old;
  }
  
  static void grow() {
    Object[] newList = new Object[list.length + (list.length >> 1)];
    for (int i = 0; i < list.length; i++) {
      newList[i] = list[i];
    }
    list = newList;
  }
}

3단계 : 두개 이상의 배열 객체를 만들 수 있도록 인스턴스 메서드로 변경하고, 접근이 필요 없는 필드들은 private 처리하기. size에 간접 접근할 수 있는 메서드 생성.

package com.eomcs.corelib.ex03;

public class MyArrayList {
  // 접근제어자 private 추가
  private Object[] list = new Object[5];
  private int size;
  
  void add(Object obj) {
    if (size == list.length) {
      grow();
    }
    list[size] = obj;
    size++;
  }
  
  void add(int index, Object obj) {
    if (index < 0 || index > size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    if (size == list.length) {
      grow();
    }
    for (int i = size - 1; i >= index; i--) {
      list[i+1] = list[i];
    }
    list[index] = obj;
    size++;
  }
  
  Object get(int index) {
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    return list[index];
  }
  
  Object set(int index, Object obj) {
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    Object old = list[index];
    list[index] = obj;
    return old;
  }
  
  Object remove(int index) {
    Object old = list[index];
    if (index < 0 || index >= size) {
      throw new ArrayIndexOutOfBoundsException("인덱스가 유효하지 않습니다.");
    }
    for (int i = index; i < size - 1; i++) {
      list[index] = list[index + 1];
    }
    size--;
    list[size] = null;
    return old;
  }
  
  private void grow() {
    Object[] newList = new Object[list.length + (list.length >> 1)];
    for (int i = 0; i < list.length; i++) {
      newList[i] = list[i];
    }
    list = newList;
  }
  
  // 사이즈 조회할 수 있게 하는 메서드
  int size() {
    return size;
  }
}