1. String 객체의 hashCode() 메서드
흔히 Java에서 참조형 변수의 메모리 주소를 파악하는 방법으로 Object 객체의 hashCode()를 사용합니다.
사실 hashCode()의 값은 실제 메모리 주소가 아닌 메모리 주소 값을 해싱된 값입니다.
그런데 String 객체는 hashCode() 메서드가1) +연산자 - immutable 오버라이딩 되어있는데, 주소가 아닌 문자열을 해싱한 값을 갖고 있습니다.
String str1 = new String("hello");사용하는 것이 좋습니다.
System.out.println(str1.hashCode()); // 99162322
String str2 = new String("hello");
System.out.println(str2.hashCode()); // 99162322
위의 두 String 객체 s1, s2는 다른 객체이기 때문에 주소값은 다를 것입니다.
일반적인 객체였다면 hashcode() 결과값이 달라야 하지만, String 객체이므로 hashCode()의 결과 값이 같다는 것을 확인할 수 있습니다.
2. ==(등가비교 연산자)와 Obejct 객체의 equals() 메서드
- == 연산자
- 동일성, 즉 두 객체의 주소가 같은지를 비교합니다.
- equals() 메서드
- 동질성, 즉 두 객체의 내용이 같은지를 비교합니다.
Object의 equals() 메서드는 hashCode() 결과값을 기반으로 두 객체를 비교합니다.
두 객체가 같으려면, 기본적으로 메모리가 같아야하기 때문이죠.
그래서 새롭게 생성한 클래스의 객체는 equals() 메서드를 오버라이딩 하지 않은 상태이므로, equals()와 == 연산자는 동일한 결과를 갖습니다.
MyClass myClass1 = new MyClass();
MyClass myClass2 = new MyClass();
System.out.println(myClass1 == myClass2); // false
System.out.println(myClass1.equals(myClass2)); // false
따라서 두 객체가 완전히 같은지, 즉 동질성 비교가 필요한 경우 equals() 메서드를 오버라이딩해야 합니다.
오버라이딩은 객체의 주소가 같은지 확인하고 객체의 내용도 비교를 하면 됩니다.
일반적으로 equals() 메서드를 오버라이딩 하면, hashCode()도 오버라이딩 하는 것이 좋다고 합니다.
그 이유는 Collection 프레임워크의 HashSet 클래스를 예로 들어서 말씀드리겠습니다.
HashSet은 같은 객체를 저장하지 않는 자료구조입니다.
HashSet에 어떤 객체를 추가할 때, 그 객체와 같은 객체가 존재하는지 확인하기 위해 모든 객체의 내용들을 비교하는 것보다, 미리 값을 해싱 해놓아서 해싱된 값들을 비교하는 것이 더 성능이 좋습니다.
대부분의 경우에는 큰 문제는 없지만, 이와 같이 성능을 끌어 올리기 위해서는 hashCode() 메서드도 오버라이딩 하는 것이 좋습니다.
( 이클립스의 source 탭 – Generate hashCode() and equals() 클릭하면 자동으로 구현 됩니다. )
3. 리터럴( Literal )과 JVM 상수 풀
String str1 = "hello";
이렇게 Sting 객체에 리터럴로 값을 선언하면, JVM의 상수 풀에서 "hello" 문자열을 해싱한 hashCode를 찾아봅니다.
해싱 값이 상수 풀에 없으면 상수 풀에 "hello" 문자열을 해싱한 값을 추가합니다.
String str2 = "hello";
이어서 같은 값을 가진 또 다른 String 객체를 선언하면, 똑같이 JVM 상수 풀에서 "hello" 문자열을 해싱한 hashCode를 찾아보는데,
위에서 "hello"를 해싱한 값을 상수 풀에 추가 했으므로 지금 추가한 문자열의 hashCode는 상수 풀에 추가하지 않고 기존 것을 참조합니다.
즉, str1과 str2 객체는 메모리에서 "hello"를 각각 참조하는 것이 아니라, 상수 풀에서 하나의 "hello"를 참조하고 있는 것입니다.
이렇게 하는 이유는 메모리를 아끼려고 하는 것입니다.
같은 문자열인데 메모리를 또 할당할 필요가 없기 때문이죠.
그래서 문자열을 생성할 때 new String() 보다 위와 같이 리터럴로써 생성하는 것이 좋습니다.
new String()은 새로운 객체를 생성하는 것이므로,
String str1 = new String("hello");
String str2 = new String("hello");
위의 str1과 str2는 각각 따로 메모리를 차지합니다.
테스트는 str1 == str2 를 실행해봄으로써 확인할 수 있습니다.
Wrapper 객체
참고로 Wrapper 객체를 생성할 때도 마찬가지로 new Integer()보다 아래와 같이 리터럴로 생성하는 것이 메모리 절약 면에서 좋습니다.
Integer number = 3;
또한 기본 타입형인 int보다 Integer를 사용하는 것이 더 좋습니다.
그 이유는 Integer는 객체라는 점과 예외처리 면에서 유연하기 때문입니다.
객체라고 해서 기본 타입형 int보다 메모리를 심각하게 더 많이 차지하는 것도 아닙니다.
흔히 객체라 생각해서 무거운 느낌을 받게 되는데, 기본 타입형보다 Wrapper 객체를 사용할 것을 권장하고 있습니다.
특히 예외처리가 필요한 웹 프로그래밍 쪽에서는 말이죠.
4. String 객체에서 +연산자를 사용할 수 있었던 이유와 immutable 특징
1) +연산자 - immutable
String str = "hello";
String str2 = str + "!";
문자열 객체는 +연산이 가능합니다.
위의 예제에서 str2 객체는 String 객체 str의 리터럴 문자열 "hello"와 "!" +연산의 결과 "hello!" 값이 저장됩니다.
이때 str2는 str 객체를 변경하는 것이 아니라, 새로운 객체를 생성합니다.
String 객체는 immutable( 변경 불가능 )이므로 객체를 변경할 수 없으므로 새로운 객체를 생성합니다.
이처럼 String 객체에 +연산을 하면, 새로운 객체를 생성하고 또 소멸시키는 과정에서 비용이 발생합니다.
2) StringBuffer, StringBuilder - mutable
이에 따라 연산의 효율성을 높일 수 있는 StringBuffer와 StringBuilder 클래스가 있습니다.
이 두 클래스는 String과 달리 mutable(변경 가능)합니다.
즉, 기존의 문자열을 변경하고자 할 때 append() 메서드를 사용하여, 객체를 새롭게 생성하지 않고 기존의 문자열을 변경할 수 있습니다.
String의 +연산자는 사실 StringBuffer의 append()와 같습니다.
즉, +연산을 하면 내부적으로 StringBuffer 객체를 생성하고 append() 메서드를 수행하게 됩니다.
그 결과 새로운 객체를 반환시키죠.
위의 예제에서는 StringBuffer 객체를 생성하는 과정이 총 3번 발생합니다.
따라서 String을 합치기 위해 +연산을 사용하는 것보다 StringBuffer, StringBuilder클래스를 사용하는 것이 String보다 빠르다고 할 수 있습니다.
예를 들어, 반복문에서 문자열을 +연산하는 횟수가 많을 경우 +연산자를 사용하는 것보다 StringBuffer.appned()를 사용하는 것이 좋을 것입니다.
그런데 대략 1000번의 +연산을 수행하는 것과 append() 메서드를 사용할 때의 성능차이는 크게 나지 않습니다.
또한 StringBuffer를 사용하는 것보다 String을 사용하는 것이 가독성이 더 좋기 때문에 String을 사용하곤 합니다.
정리하면, 일반적인 경우에서는 String을 사용하고 성능적인 문제를 고려해야 한다면 StringBuffer를 사용하는 것이 좋을 것 같습니다.
5. java는 call by value인가? call by reference인가?
Java가 call by value인지 call by ref 일까요?
결론부터 말씀드리면 둘 다 입니다.
1) call by value
public static void main(String[] args) {
int a = 10;
change(a);
System.out.println(a); // 10
}
public static void change(int a) {
a = 20;
}
change() 메서드를 호출해서 변수 a의 값인 10을 20으로 바꾸려고 했지만, 그 결과는 그대로 10입니다.
그 이유는 기본형 타입을 인자로 전달할 때는 call by value가 되기 때문입니다.
2) call by reference
public class Main {
public static void main(String[] args) {
MyClass myClass1 = new MyClass(10);
MyClass myClass2 = new MyClass(20);
swap(myClass1, myClass2);
System.out.println(myClass1.num + "," + myClass2.num); // 20, 10
}
public static void swap(MyClass myClass1, MyClass myClass2){
int tmp = myClass1.num;
myClass1.num = myClass2.num;
myClass2.num = tmp;
}
}
class MyClass {
int num = 0;
MyClass(int num) {
this.num = num;
}
}
myClass1과 myClass2 객체의 변수 num에 각각 10과 20을 할당하고, 이를 교환하는 코드입니다.
이번에는 의도한대로 값이 변경되었습니다.
변경된 이유, 즉 call by value와 차이점은 무엇일까요?
바로 인자로 전달하는 데이터 타입에 있습니다.
call by ref 경우에는 인자로 객체를 전달했습니다.
정리하면, 인자로 기본형 타입을 전달하느냐, 참조형을 전달하느냐에 따라 call by value가 되느냐 call by ref가 되느냐가 됩니다.
6. try-catch를 사용하는 것은 좋은가?
try - catch는 예외가 발생할 경우 프로그램을 종료시키지 않고, 예외에 대한 처리를 할 수 있도록 하는 기법입니다.
즉, 흐름 제어를 통해 애플리케이션의 안정성을 확보할 수 있습니다.
하지만 try - catch에는 두 가지 단점이 있습니다.
- 블록{} 이 많기 때문에 가독성이 떨어진다.
- try - catch는 호출스택을 저장해두기 때문에 비용이 많이 드는 로직입니다.
제가 하고 싶은 말은 try-catch를 남용하지 말자는 것입니다.
Arithmeticexception(산술 연산에 대한 예외)을 예로 들어 그 이유를 말씀드리겠습니다.
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
int b = sc.nextInt();
divide(a, b);
}
public static int divide(int a, int b) {
return a/b;
}
사용자의 입력 값을 받아 나눗셈을 하는 코드입니다.
만약에 b의 값을 0으로 입력하면 어떻게 될까요?
Arithmeticexception이 발생하면서 프로그램이 종료됩니다.
그래서 Arithmeticexception은 예외니까 try-catch를 사용하려고 하는데, 좋은 방법일까요?
그보다는 조건문으로 b가 0이면 다시 입력을 하도록 해결하는 것이 더 바람직해 보입니다.
즉, Arithmeticexception처럼 프로그램 로직이 잘못되서 발생하는 예외들은 try - catch를 사용하지 말고 조건문을 통해 로직을 짜는것이 바람직합니다.
정리하면, try-catch는 로직은 완벽하지만 네트워크 통신, 파일 IO 오류처럼 예상치 못한 외부 요인으로 인해 예외가 발생할 때 사용하는 것이 좋습니다.
위의 내용과 무관하지만, try - catch에서 finally는 자원정리를 하고 싶을 때 사용하는 것이 좋습니다.
예를 들어, 연결을 종료하거나 IO stream을 종료하는 과정은 finally에 작성하는 것이 바람직합니다. ( try - catch - finally )
7. Collection Framework 정리
Collection Framework은 Java에서 제공하는 자료구조로서, List / Set / Map 인터페이스가 존재합니다.
1) Vector
이 외에 Vector라는 것이 있는데, Vector는 Java1.0 ~ java1.2까지 주로 사용되었고, 이후에 List인터페이스를 구현하도록 되었습니다.
그래서 Vector에도 List 인터페이스의 추상메서드인 add, get, remove 등이 존재하며,
Vector에서 원래 사용하고 있던 메서드인 addElement, removeAt이 그대로 사용되고 있어서, 같은 기능을 하는 다른 이름의 메서드들이 공존하고 있습니다.
2) Vector와 List 비교
Vector와 List는 비슷한데, 차이점은 Vector는 동기처리가 된다는 것입니다.
멀티쓰레드 프로그래밍에서 JVM의 인스턴스 변수는 여러 쓰레드가 동시에 접근할 가능성이 있으므로 일관성을 유지하기 위해 동기처리가 필요합니다.
( 쓰레드마다 Stack이 생성되지만 Heap영역은 서로 공유하기 때문에, 인스턴스 변수에 동시 접근이 가능 )
하지만 Vector는 동기처리가 되어있기 때문에 이를 효율적으로 관리할 수 있습니다.
그러나 동기화 처리하는 부분은 코드에서 명시하는 것이 가독성에 좋으므로, 굳이 Vector를 쓰는 것을 선호하지는 않습니다.
정리하면, Vector말고 List를 사용하자는 것입니다.
3) Collection framework 클래스
Collection framework에서 주로 사용하는 클래스는 아래와 같습니다.
- List 인터페이스를 구현한 클래스
- Vector , ArrayList , LinkedList
- Set 인터페이스를 구현한 클래스
- HashSet
- Map 인터페이스를 구현한 클래스
- HashMap, HashTable
- HashTable도 Vector와 같이, 옛날코드이며 동기화가 되어 있습니다.
4) 이중 배열처럼 List안에 List 추가하기
List<List<Object>> list = new ArrayList<Object>();
이상으로 Java의 특징정인 부분을 정리해봤습니다.
너무 마구잡이로 주제를 다룬것 같아 조금 아쉽네요.
'웹 프로그래밍 > ------ Java ' 카테고리의 다른 글
[Java] JDBC 사용하기 ( MySQL ) (12) | 2018.03.11 |
---|---|
[Java] 채팅 프로그램( 소켓 프로그래밍, 멀티 쓰레드 ) (31) | 2018.03.11 |
[Java] 핵심정리 (2) - File I/O ( Stream ) (0) | 2018.03.10 |