java.lang패키지는 자바에서 가장 기본이 되는 클래스를 포함하고 있다. 그래서 import문 없이도 패키지명을 생략하여 사용할 수 있게끔 했다. 패키지 내의 여러 클래스 중에서도 가장 많이 사용되는 Object, String, StringBuffer, Wrapper 클래스를 학습하였다.
Object 클래스는 모든 클래스의 최고 부모 클래스이기 때문에 어떤 클래스에서건 Object 클래스의 멤버들을 바로 사용할 수 있다. 그 중 몇가지 메서드를 살펴보자.
equal 메서드는 맴버변수로 객체의 참조변수를 받아 비교한 후 boolean으로 리턴하는 함수이다.
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1==s2); // false
System.out.println(s1.equals(s2)); // true
위의 코드를 보면, s1==s2는 false를 반환하였다. 그 이유는 s1,s2에는 각각 인스턴스를 참조하는 참조변수이기 때문에 각 인스턴스의 주소값이 저장되어 있다. 두 변수의 주소값은 다르기 때문에 false를 반환한다.
반면 equals 메서드를 사용하면 true를 반환하는데 그 이유는 주소값을 비교한 것이 아니라 문자열을 비교했기 때문이다. 사실 이는 String 클래스만이 가지고 있는 예외적인 현상이다. 원래의 equal 메서드는 각 변수에 저장되어있는 값을 비교하는 것이 맞는데, String 클래스에서는 문자열을 비교하도록 오버라이딩되어있기 때문이다. 오버라이딩되어있지 않은 일반적인 클래스에서는 두 참조 변수를 비교하면 false를 반환할 것이다.
package sample;
public class Sample {
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = new Value(10);
System.out.println(v1.equals(v2)); // false
}
}
class Value {
int value;
Value(int value) {
this.value = value;
}
}
혹시 String 클래스처럼 equal 메서드를 오버라이딩하여 참조변수의 주소값을 비교하는 것이 아닌, 멤버변수를 비교하도록 하려면 어떻게 오버라이딩해야할까?
package sample;
public class Sample {
public static void main(String[] args) {
Value v1 = new Value(10);
Value v2 = new Value(10);
System.out.println(v1.equals(v2)); // true
}
}
class Value {
int value;
Value(int value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Value) {
return value == (((Value) obj).value);
} else return false;
}
}
Value 클래스에 equals 메서드를 오버라이딩했다. 조건문을 사용해서 매개변수로 들어오는 객체가 Value 클래스의 인스턴스라면 Value 클래스의 멤버변수인 value가 매개변수로 들어온 obj 객체의 value가 같다는 것(true)를 리턴하도록 했다. 여기서 매개변수 obj는 Object타입이기 때문에 Value 타입으로 형변환을 해줘야 멤버변수를 호출할 수 있다.
이 메서드는 인스턴스에 대한 정보를 문자열로 반환한다. 일반적인 인스턴스에서 toString 메서드를 호출하면 인스턴스의 클래스 이름과 해시코드(주소값)가 출력된다. 적절히 오버라이딩을 하여 인스턴스에 저장된 멤버변수값을 반환하도록 할 수 있다. 위에 사용했던 코드에서 toString 메서드를 오버라이딩하면
@Override
public String toString() {
return "Value{" +
"value=" + value +
'}';
}
Value v1 = new Value(10);
System.out.println(v1); // Value{value=10}
모든 메서드를 오버라이딩할 때도 마찬가지겠지만 toString 메서드의 접근제어자는 public이다. 따라서 자식 클래스에 오버라이딩할 때는 접근제어자를 더 같거나 넓은 범위로 해야한다.
이 메서드는 자신을 복제하여 새로운 인스턴스를 생성하는 역할을 한다. 원래의 인스턴스는 보존하고 복제 인스턴스로 작업을 하다가도 실패하게 되면 원래의 인스턴스를 보존하고 있기 때문에 유용하다.
clone 메서드를 사용하려면 clone 메서드를 호출할 클래스에 Cloneable(복제할 수 있는) 인터페이스를 구현해야 한다. 그 이유는 데이터를 보호하기 위함이다. 클래스에 Cloneable 인터페이스가 구현되어 있다는건 클래스 작성자가 복제를 허용해 준다는 말이다.
package sample;
public class Point implements Cloneable{
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
this(0, 0);
}
@Override
public Object clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("clone fail");
}
return obj;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
package sample;
public class Sample {
public static void main(String[] args) {
Point p1 = new Point(1,1);
Point p2 = (Point) p1.clone();
System.out.println(p1); // Point{x=1, y=1}
System.out.println(p2); // Point{x=1, y=1}
}
}
오버라이딩한 clone메서드에 대해 설명하자면 Object 타입의 참조변수를 하나 선언한다. clone 메서드를 사용하려면 반드시 예외처리를 해주어야한다. Object 타입의 참조변수에 조상클래스인 Object클래스(super)의 clone 메서드를 호출해 저장한다.
현재는 자바가 업데이트되어 오버라이딩을 할 때, 리턴타입을 꼭 부모 클래스와 똑같이 할 필요없고 자손 클래스의 타입으로 변경을 허용하도록 바뀌었다.
@Override
public Point clone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("clone fail");
}
return (Point) obj;
}
Point p2 = p1.clone();
이렇게 clone메서드의 부모 클래스인 Object 타입이 아니라 자식 클래스인 Point로 리턴 타입을 바꾸면 main메서드에서 clone메서드를 호출할 때 호출한 후에 타입을 안바꿔줘도 된다.
clone 메서드는 단순히 객체에 저장되어 있는 값만을 복제할 뿐, 객체 자체를 복제하지는 않는다. 문제는 참조변수를 복제하는 경우 생긴다. 주소값을 복제하기 때문에 멤버 변수의 값을 변경해야 하는 경우 두 인스턴스가 같이 변경된다. 이런 문제점 역시 적절히 오버라이딩을 함으로써 독립적인 인스턴스 복제가 가능하다.
package sample;
public class Circle implements Cloneable{
Point p;
double r;
Circle(Point p, double r) {
this.p = p;
this.r = r;
}
public Circle shallowClone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Exception");
}
return (Circle) obj;
}
public Circle deepClone() {
Object obj = null;
try {
obj = super.clone();
} catch (CloneNotSupportedException e) {
System.err.println("Exception");
}
Circle c = (Circle) obj;
c.p = new Point(this.p.x, this.p.y);
return c;
}
public void move(int i, int j) {
p.x += i;
p.y += j;
}
@Override
public String toString() {
return "Circle{" +
"p=" + p +
", r=" + r +
'}';
}
}
package sample;
public class Sample {
public static void main(String[] args) {
Circle c1 = new Circle(new Point(1, 1), 2);
Circle c2 = c1.shallowClone();
Circle c3 = c1.deepClone();
System.out.println(c1);
// Circle{p=Point{x=1, y=1}, r=2.0}
System.out.println(c2);
// Circle{p=Point{x=1, y=1}, r=2.0}
System.out.println(c3);
// Circle{p=Point{x=1, y=1}, r=2.0}
c1.move(1,1);
System.out.println(c1);
// Circle{p=Point{x=2, y=2}, r=2.0}
System.out.println(c2);
// Circle{p=Point{x=2, y=2}, r=2.0}
System.out.println(c3);
// Circle{p=Point{x=1, y=1}, r=2.0}
}
}
주소값만을 복제하냐 객체 자체를 복제하냐에 따라 shallowClone메서드, deepClone메서드로 나누었다. shallowClone메서드는 위의 코드와 같이 Object 타입의 변수에 clone을 호출하여 저장된 주소값을 그대로 형변환만 하여 리턴했다. 반면, deepClone메서드는 형변환한 obj를 Circle 타입의 변수에 새로 저장하고 인스턴스 변수인 Point 클래스의 인스턴스도 새로 생성해 준 후에, Circle타입의 변수를 리턴하였다. 따라서, 다른 주소에 똑같은 값을 갖는 객체가 새로 생겼기 때문에 객체도 복제가 가능해졌다.
자체적으로 만든 move메서드를 이용해서 c1의 Point 인스턴스를 바꿔준 후에 다시 출력해보면, c2는 c1주소값이 복사되었기 때문에 변경된 값이 c1과 c2가 똑같고, c3는 객체를 새로 생성했기 때문에 주소가 달라서 c3는 변경되지 않는다.
지금까지 String 클래스를 이용해서 인스턴스를 수정하고 더하는 등의 작업을 해왔다. 근데 사실은 인스턴스의 값이 변경된 것이 아니라, 새로운 인스턴스가 생성된 것이였다.
a = "a";
b = "b";
a = a+ b; // ab
이 때, "a"문자열이 위치한 주소값이 저장되어 있던 a인스턴스가 그 해당 주소값에 문자열이 "ab"로 변경되는 것이 아니라, 새로운 인스턴스가 생성되고 그 안에 "ab"가 위치한 주소값이 저장되는 것이다. 따라서 문자열을 다루는 작업을 많이 한다면, String 클래스 대신 mutable(변경 가능한)한 클래스인 StringBuffer를 이용하면 메모리를 효율적으로 쓸 수 있다.
String s1 = "abc";
String s2 = new String("abc");
String s3 = "abc";
String s4 = new String("abc");
문자열 리터럴을 이용하는 것("")은 클래스가 메모리에 로드될 때 자동적으로 미리 생성되기 때문에 이미 존재하는 것을 재사용하는 것이다. 반면, 생성자를 이용하는 경우엔 인스턴스 생성하는 new 연산자로 인해 메모리할당이 이루어지기 때문에 새로운 인스턴스가 생성된다. 이를 검증할 수도 있는데 equals 메서드를 사용하여 비교하면 새로운 인스턴스가 생겼다는걸 알 수 있다.
System.out.println(s1==s3);
// true - 문자열이 같으면 같은 주소값을 가집
System.out.println(s1.equals(s3);
// true - 문자열이 같아서 true
System.out.println(s2==s4);
// false - 새로운 인스턴스가 생겼다는 의미(주소값 달라서 false)
System.out.println(s2.equals(s4));
// true - 문자열이 같아서 true
길이가 0인 문자열도 존재한다. 일반적으로 변수를 선언할 때, 각 타입의 기본값으로 초기화하기도 하지만, String은 null보다는 빈문자열""로, char형은 /u0000 대신 공백' '으로 초기화하기도 한다.
처음 보는 메서드이거나 아직 익숙하지 않은 것들을 위주로 정리하였다.
// 1. 매개변수로 char[]
char[] c = {'a','b','c'};
String str1 = new String(c); // abc
// 2. StringBuffer 인스턴스의 문자열로 String 인스턴스 생성
StringBuffer sb1 = new StringBuffer("hello");
String str2 = new String(sb1); // hello
// 3. 지정된 위치의 문자 반환
str2.charAt(2); // l
// 4. 문자열 사전순으로 비교
int i = "aa".compareTo("bb"); // 매개변수보다 앞이므로 -1
int i2 = "aa".compareTo("aa"); // 같으면 0
int i3 = "bb".compareTo("aa"); // 매개변수보다 뒤면 1
// 5. 문자열 뒤에 덧붙이는 메서드
String str3 = str2.concat(" world"); // hello world
// 6. 매개변수의 위치 반환
int i4 = str3.indexOf('e'); // 1
int i5 = str3.lastIndexOf('l'); // 제일 끝의 l의 인덱스 반환
// 7. 주어진 위치의 문자열 반환
String str4 = str3.substring(6); // world
// 8. 공백제거(문자열 안의 공백은 제거 못함)
String str5 = " abc ";
str5.trim(); // "abc"
// 9. 다른 타입을 String 타입으로 형변환
int i6 = 123;
String str6 = String.valueOf(i6); // "123"
// 10. 여러 문자열 사이에 구분자()를 넣어서 결합(split과 반대)
String str7 = "abc,def,ghi";
String[] strArr = str7.split(","); // {"abc","def","ghi"}
String str8 = String.join("~",str7); // "abc~def~ghi"
// 11. 여러 문자열 사이에 구분자()를 넣어서 결합
StringJoiner sj = new StringJoiner(",","<",">");
for (String s : strArr) {
sj.add(s.toUpperCase());
}
System.out.println(sj.toString()); // <ABC,DEF,GHI>
위에 언급했듯이 String.valueOf를 사용할 수도 있지만, 성능 향상에는 도움이 되지만 간단한걸로 치면 빈문자열을 더하는 것이 훨씬 편하다.
int i = 100;
String str1 = String.valueOf(i); // "100"
String str2 = i + ""; // "100"
다른 기본형 타입도 String.valueOf 메서드를 이용해서 변환할 수 있다.
int를 String으로 바꾸는 방법은 많이 해봐서 알고있다.
다른 기본형 타입도 비슷한 방법으로 String으로 바꾼다.
(parse 분석하다.)
String 변수에 공백이 있는 경우 에러가 날 수 있으므로 trim() 메서드를 사용하자. 부호를 의미하는 +나 f,L과 같은 자료형 접미사는 알맞은 변환을 하는 경우에 한해 허용된다.
// 메서드 선언부
Integer.parseInt(String s)
Float.parseFloat(String s)
Boolean.parseBoolean(String s)
...
StringBuffer는 이전에 설명 했듯이 String과 달리 인스턴스에 저장된 문자열의 변경이 가능하다. 문자열 사용이 잦은 코드를 짤 때, 메모리를 효율적으로 관리하기 위해 StringBuffer를 사용하는 것이 좋다.
StringBuffer는 String과 같이 char 배열 참조변수를 인스턴스 변수로 선언해 놓았다. 인스턴스를 생성하면, 적절한 길이의 char 배열이 생성된다. 이 배열은 문자열을 저장하고 편집하기 위한 공간(Buffer)로 사용된다.
StringBuffer의 메서드 상당수가 자신의 기능을 한 후 리턴값으로 자신의 주소값을 반환하기 때문에 메서드를 한줄로 이어서 쓰는 것이 가능하다.
StringBuffer sb1 = new StringBuffer("hello");
sb1.append(" world").append(". hello").append(" java.");
System.out.println(sb1); // hello world. hello java.
// StringBuffer는 toString이 오버라이딩 되어있다.
객체지향 개념에서 모든 것은 객체로 다루어져야 하지만 자바에서는 기본형 타입 8개를 객체로 다루지 않는다. 그래서 자바가 객체지향언어가 아니라는 소리도 듣지만 덕분에 높은 성능을 얻을 수 있게 되었다.
때로는 기본형(primitive) 타입도 어쩔 수 없이 객체로 다루어야 하는 경우가 있다. 예를 들어, 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야 할 때, 등의 경우에는 기본형 값을 객체로 바꾸어 줘야 한다. 이 때 사용되는 것이 Wrapper class이다.
Integer i1 = new Integer(100);
Integer i2 = new Integer(100);
System.out.println(i1 == i2); // false
System.out.println(i1.equals(i2)); // true(equals 오버라이딩)
System.out.println(i1); // toString 오버라이딩
System.out.println(Integer.MAX_VALUE); // 2147483647
원래는 기본형과 참조형(래퍼 클래스 등)의 연산이 불가능했지만 현재는 연산이 가능하다.
Integer i1 = new Integer(100);
int i2 = 100;
int sum = i1 + i2; // 200
// int sum = i1.intValue() + i2;
// 컴파일러가 Integer객체에 intValue메서드 붙여서
// int 타입으로 변환해줌
기본형을 래퍼클래스 객체로 자동변환되는 것을 오토박싱, 반대를 언박싱이라고 한다.