네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'2021/10'에 해당되는 글 16건


반응형

ECMA Script 6: let, const와 블록 레벨 스코프

ES6까지 변수를 선언할 수 있는 유일한 방법은 var 키워드를 사용하는 것이었다.

 

# var 키워드로 선언된 변수의 특징

1. 함수 레벨 스코프 (Function-level scope)

- 함수의 코드 블록만을 스코프로 인정한다. 따라서 전역 함수 외부에서 생성한 변수는 모두 전역변수이다. 이는 전역 변수를 남발할 가능성을 높인다.

- for문의 변수 선언문에서 선언한 변수를 for문의 코드 블록 외부에서 참조할 수 있다.

2. var 키워드 생략 허용

- 암묵적 전역 변수를 양산할 가능성이 크다.

3. 변수 중복 선언 허용

- 의도하지 않은 변수 값의 변경이 일어날 가능성이 크다.

4. 변수 호이스팅

- 변수를 선언하기 이전에 참조할 수 있다.

 

=> 대부분의 문제는 전역 변수로 인해 발생한다. 전역 변수는 간단한 애플리케이션의 경우, 사용이 편리하다는 장점이 있지만 불가피한 경우를 제외하고 사용을 억제해야 한다. 전역변수는 유효 범위(scope)가 넓어서 어디에서 어떻게 사용될지 파악하기 힘들며, 비수순수 함수(Impure function)에 의해 의도하지 않게 변경될 수 있어서 복잡성을 증가시키는 원인이 된다. 따라서 변수의 스코프는 좁을수록 좋다.

=> ES6는 이러한 var키워드의 단점을 보완하기 위해 let과 const 키워드를 도입하였다.

 

1. let

### 1.1 블록 레벨 스코프

대부분의 프로그래밍 언어는 블록 레벨 스코프 (Block-level scope)를 따르지만 자바스크립트는 함수 레벨 스코프(Function-level scope)를 따른다.

* 함수 레벨 스코프
함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다. 즉, 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.
* 블록 레벨 스코프
모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없다. 즉, 코드 블록 내부에서 선언한 변수는 지역 변수이다.
var foo = 123; // 전역 변수
console.log(foo); // 123

{
	var foo = 456; // 전역 변수
}
console.log(foo); // 456

블록 레벨 스코프를 따르지 않는 var 키워드의 특성 상, 코드 블록 내의 변수 foo는 전역 변수이다. 그런데 이미 전역 변수 foo가 선언되어 있다. var 키워드를 사용하여 선언한 변수는 중복 선언이 허용되므로 위의 코드는 문법적으로 아무런 문제가 없다. 단, 코드 블록 내의 변수 foo는 전역 변수기 때문에 전역에서 선언된 전역 변수 foo의 값 123을 새로운 값인 456으로 재할당하여 덮어쓴다.

 

=> ES6는 블록 레벨 스코프를 따르는 변수를 선언하기 위해 let 키워드를 제공한다.

let foo = 123; // 전역 변수
{
	let foo = 456; // 지역 변수
    let bar = 456; // 지역 변수
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined

let 키워드로 선언된 변수는 블록 레벨 스코프를 따른다. 위 예제에서 코드 블록 내에 선언된 변수 foo는 블록 레벨 스코프를 갖는 지역 변수이다. 전역에서 선언된 변수 foo와는 다른 별개의 변수이다. 또한 변수 bar도 블록 레벨 스코프를 갖는 지역 변수이다. 따라서 전역에서는 변수 bar를 참조할 수 없다.

 

### 1.2 변수 중복 선언 금지

var 키워드로는 동일한 이름을 갖는 변수를 중복해서 선언할 수 있었다. 하지만, let 키워드로는 동일한 이름을 갖는 변수를 중복해서 선언할 수 없다. 변수를 중복 선언하면 문법 에러(SyntaxError)가 발생한다. 

var foo = 123;
var foo = 456; // 중복 선언 허용

let bar = 123;
let bar = 456; // Uncaught SyntaxError: Identifier 'bar' has already been declared

 

### 1.3 호이스팅(Hoisting)

자바스크립트는 ES6에 도입된 let, const를 포함하여 모든 선언 (var, let, const, function, function*, class)를 호이스팅한다.

* 호이스팅(Hoisting)
var 선언문이나 function 선언문 등을 해당 스코프의 선두로 옮긴 것처럼 동작하는 특성

하지만, var 키워드로 선언된 변수와 달리 let 키워드로 선언된 변수를 선언문 이전에 참조하면 참조 에러(ReferenceError)가 발생한다. 이는 let 키워드로 선언된 변수는 스코프의 시작에서 변수의 선언까지 일시적 사각지대(Temporal Dead Zone; TDZ)에 빠지기 때문이다.

console.log(foo); // undefined
var foo;

console.log(bar); // Error: Uncaught ReferenceError: bar is not defined
let bar;

변수가 어떻게 생성되며 호이스팅은 어떻게 이루어지는지...

변수는 3단계에 거쳐 생성된다. (Execution Context)

* 선언 단계 (Declaration phase)
- 변수를 실행 컨텍스트의 변수 객체 (Variable Object)에 등록한다. 이 변수 객체는 스코프가 참조하는 대상이 된다.
* 초기화 단계 (Initialization phase)
- 변수 객체 (Variable Object)에 등록된 변수를 위한 공간을 메모리에 확보한다. 이 단계에서 변수는 undefined로 초기화된다.
* 할당 단계 (Assignment phase)
- undefined로 초기화된 변수에 실제 값을 할당한다.

var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어진다. 즉, 스코프에 변수를 등록(선언 단계)하고 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화한다. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 변수가 존재하기 때문에 에러가 발생하지 않는다. 다만 undefined를 반환한다. 이후 변수 할당문에 도달하면 비로소 값이 할당된다. 이러한 현상을 변수 호이스팅(Variable Hoisting)이라 한다.

// 스코프의 선두에서 선언 단계와 초기화 단계가 실행된다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 있다.
console.log(foo); // undefined

var foo;
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행된다. 즉, 스코프에 변수를 등록(선언 단계)하지만 초기화 단계는 변수 선언문에 도달했을 때 이루어진다. 초기화 이전에 변수에 접근하려고하면 참조 에러(ReferenceError)가 발생한다. 이는 변수가 아직 초기화되지 않았기 때문이다. 다시 말하면 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문이다. 따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다. 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 '일시적 사각지대(Temporal Dead Zone; TDZ)' 라고 부른다.

// 스코프의 선두에서 선언 단계가 실행된다.
// 아직 변수가 초기화(메모리 공간 확보와 undefined로 초기화)되지 않았다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 없다.
console.log(foo); // ReferenceError: foo is not defined
// 일시적 사각지대 (TDZ)

let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

결국 ES6에서 호이스팅이 발생하지 않는 것과 차이가 없어 보이지만 그렇지 않다.

let foo = 1; // 전역 변수

{
	console.log(foo); // ReferenceError: foo is not defined
    let foo = 2; // 지역 변수
}

전역 변수 foo의 값이 출력될 것처럼 보이지만 ES6의 선언문도 여전히 호이스팅이 발생하기 때문에 참조 에러(ReferenceError)가 발생한다.

ES6의 let으로 선언된 변수는 블록 레벨 스코프를 가지므로 코드 블록 내에서 선언된 변수 foo는 지역 변수이다. 따라서 지역 변수 foo도 해당 스코프에서 호이스팅되고 코드 블록의 선두부터 초기화가 이루어지는 지점까지 일시적 사각지대(TDZ)에 빠진다. 따라서 전역 변수 foo의 값이 출력되는 것이 아니라 참조 에러가 발생한다.

 

### 1.4 클로저

블록 레벨 스코프를 지원하는 let은 var보다 직관적이다.

var funcs = [];

// 함수의 배열을 생성하는 for 루프의 i는 전역 변수
for (var i = 0; i < 3; i++) {
	funcs.push(function () { console.log(i); });
}

// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) {
	funcs[j]();
}

위 코드의 실행 결과로 0, 1, 2를 기대하겠지만 3이 세번 출력된다. for 루프의 var i가 전연 변수이기 때문이다. 0, 1, 2를 출력하려면 아래와 같은 코드가 필요하다.

var funcs = [];

// 함수의 배열을 생성하는 for 루프의 i는 전역 변수
for (var i = 0; i < 3; i++) {
	(function (index) {
		funcs.push(function () { console.log(index); });
    }(i));
}

// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) {
	funcs[j]();
}

자바스크립트의 함수 레벨 스코프로 인하여 for 루프의 초기화 식에 사용된 변수가 전역 스코프를 갖게 되어 발생하는 문제를 회피하기 위해 클로저를 활용하는 방식이다.

ES6의 let 키워드를 for 루프의 초기화 식에 사용하면 클로저를 사용하지 않아도 위 코드와 동일한 동작을 한다. 

var funcs = [];

// 함수의 배열을 생성하는 for 루프의 i는 for 루프의 코드 블록에서만 유효한 지역 변수이면서 자유 변수이다.
for (let i = 0; i < 3; i++) {
	funcs.push(function () { console.log(i); });
}

// 배열에서 함수를 꺼내어 호출한다.
for (var j = 0; j < 3; j++) {
	console.dir(funcs[j]);
	funcs[j]();
}

for 루프의 let i 는 for 루프에서만 유효한 지역 변수이다. 또한 i는 자유 변수로 for 루프의 생명주기가 종료되어도 변수 i를 참조하는 함수가 존재하는 한 계속 유지된다.

 

## 1.5 전역 객체와 let

전역 객체 (Global Object)는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window 객체, Server-side(Node.js)에서는 global 객체를 의미한다. var 키워드로 선언된 변수를 전역 변수로 사용하면 전역 객체의 프로퍼티가 된다.

var foo = 123; // 전역 변수
console.log(window.foo); // 123

let 키워드로 선언된 변수를 전역 변수로 사용하는 경우, let 전역 변수는 전역 객체의 프로퍼티가 아니다. 즉, window.foo와 같이 접근할 수 없다. let 전역 변수는 보이지 않는 개념적인 블록 내에 존재하게 된다.

let foo = 123; // 전역 변수
console.log(window.foo); // undefined

 

 

 

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# Comparable을 구현할지 고려하라.

- Comparable을 구현하는 이유는 클래스의 인스턴스들 간의 ordering을 목적으로 구현하는 클래스이다. 따라서 Comparable을 구현한 클래스에 대한 배열은 다음처럼 손쉽게 정렬할 수 있다.

Arrays.sort(arr);

 

 

- 사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거타입은 Comparable을 구현했다. 알파벳, 숫자, 연대와 같이 순서가 명확한 값 클래스를 작성한다면, 반드시 Comparable 인터페이스를 구현하자 !

 

## 1. compareTo 메서드 규약

- 이 객체와 주어진 객체의 순서를 비교한다.

- 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 리턴한다.

- 이 객체와 비교할 수 없는 타입이 주어지면 ClassCaseException을 던진다.

 

1) 대칭성

- Comparable을 구현한 클래스는 모든 x, y에 대해 x.compareTo(y) == y.compareTo(x) * -1을 만족해야 한다.

- 따라서, x.compareTo(y)가 예외를 던지는 경우, y.compareTo(x)도 예외를 던져야 한다.

 

2) 추이성

- Comparable을 구현한 클래스는 모든 x, y, z에 대해 x.compareTo(y) > 0 이고 y.compareTo(z)이면, x.compareTo(z) > 0을 만족해야 한다.

 

3) 반사성

- Comparable을 구현한 클래스는 모든 x, y, z에 대해 x.compareTo(y) == 0 이면, sgn(x.compareTo(z)) == sgn(y.compareTo(z))를 만족해야 한다.

 

4) 동치 equals

- Comparable을 구현한 클래스는 모든 x, y에 대해 x.compareTo(y) == 0이면, x.equals(y)를 만족하는 것이 좋다. (권고사항은 아니다.) 이 권고를 지키지 않으려면 ```주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다.```라고 명시해주자.

 

## 2. compareTo 안티패턴

- Comparable은 타입을 인수로 받는 제네릭 인터페이스이다. 메서드의 인수타입은 컴파일 시점에 정해진다.

- compareTo 메서드는 필드의 동치가 아니라 순서를 비교한다. Comparable을 구현하지 않았다면 Comparator를 사용할 수 있다.

- compareTo 메서드에서 관계연산자 (< 와 >)를 사용하지 말아야 한다.

- 대신 Type.compare(T t1, T t2)를 사용하여 비교하는 것이 좋다.

* 안티 패턴

public int compareTo(int x, int y) {
	return x < y ? (x == y) ? 0 : -1;
}

 

* 아래 방법을 사용하자.

Integer.compare(a, b);
Float.compare(a, b);
Double.compare(a, b);

 

- hashCode의 차를 이용한 비교는 안된다. (추이성에 위배된다.)

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	(Object o1, Object o2) -> o1.hashCode() - o2.hashCode();
}

 

- 위의 코드를 실행하면 정수 overflow를 일으키거나 IEEE754 부동소수점 계산방식에 따른 오류를 발생시킬 수 있다.

- 아래와 같이 사용하자.

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	(Object o1, Object o2) -> Integer.compare(o1.hashCode(), o2.hashCode());
}
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

 

 

## 3. 사용 예

1) 기본 타입 필드가 여러개일 때 비교자

public int compareTo(PhoneNumber pn) {
	int result = Short.compare(this.areaCode, pn.areaCode);
	if (result == 0) {
		result = Short.compare(this.prefix, pn.prefix);
		if (result == 0) {
			result = Short.compare(this.lineNum, pn.lineNum);
		}
	}
	return result;
}

 

- 필드의 정렬 우선순위를 정해 같은 값이 있을 때마다 조건을 추가한다.

 

2) 비교자 생성 메서드를 이용한 비교자

private static final Comparator<PhoneNumber> COMPARATOR
	= comparingInt((PhoneNumber pn) -> pn.areaCode)
		.thenComparingInt(pn -> pn.prefix)
		.thenComparingInt(pn -> pn.lineNum);

 

- comparingInt라는 static 메서드를 import하여 사용하고, 두번째 조건부터 thenComparingInt를 사용하여 비교자를 추가할 수 있다.

- 최초 사용시, PhoneNumber pn을 사용하여 람다식에서 타입을 추론할 수 있도록 코드를 추가하였다.

- thenComparingInt 부터는 자바 컴파일러가 충분히 타입을 추론할 수 있으므로 명시적으로 지정하지 않았다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# clone 재정의는 주의해서 진행하라.

- Cloneable은 복제해도 되는 인터페이스임을 명시하는 용도의 mixin 인터페이스이다. 하지만 아쉽게도 의도한 목적을 이루지 못했다. 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected로 되어있다. 즉, Cloneable을 구현하는 것만으로는 외부에서 clone 메서드를 호출할 수 없다.

 

## 1. Cloneable 인터페이스

/**
* A class implements the <code>Cloneable</code> interface to
* indicate to the {@link java.lang.Object#clone()} method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
* <p>
* Invoking Object's clone method on an instance that does not implement the
* <code>Cloneable</code> interface results in the exception
* <code>CloneNotSupportedException</code> being thrown.
* <p>
* By convention, classes that implement this interface should override
* {@code Object.clone} (which is protected) with a public method.
* See {@link java.lang.Object#clone()} for details on overriding this
* method.
* <p>
* Note that this interface does <i>not</i> contain the {@code clone} method.
* Therefore, it is not possible to clone an object merely by virtue of the
* fact that it implements this interface. Even if the clone method is invoked
* reflectively, there is no guarantee that it will succeed.
*
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since 1.0
*/
public interface Cloneable {
}

 

- 자바의 Cloneable 인터페이스를 보면 아무런 메서드도 없다.

- 아무것도 없지만, 사실은 Object의 clone 메서드의 동작방식을 결정한다.

- Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면, 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면, ClassNotSupportedException을 던진다.

 

## 2. Object 클래스의 clone 규약

@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;

 

- Object에 명시된 clone 규약이 주석으로 쓰여져 있다.

- x.clone() != x 는 참이다. 복사한 객체와 원본 객체는 서로 다른 객체이다.

- x.clone().getClass() == x.getClass()는 일반적으로 참이다. 하지만 반드시 만족해야하는 것은 아니다. 관례상, 이 방법으로 반환된 객체는 독립성이 있어야 한다. 이를 만족하려면 super.clone()으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정할 수도 있다.

- x.clone().equals(x)는 참이다. 복사한 객체와 원본 객체는 논리적 동치성이 같다.

- Cloneable을 구현하지 않은 경우, CloneNotSupportedException이 throw된다.

- 모든 Array는 Cloneable을 구현하도록 고려되었다. clone() 메서드는 T[] (T는 기본타입 또는 참조타입)를 리턴하도록 설계되었다.

- 기본적으로 Object.clone()은 clone 대상 클래스에 대해 새로운 객체를 new로 생성

- 모든 필드들에 대해 초기화를 진행한다.

- 하지만 필드에 대한 객체를 다시 생성하지 않는 Shallow copy 방식으로 수행한다. (deep copy 아님.)

- 클래스에 대한 복제본을 원하지 않는 경우 clone 메서드를 재정의하여 CloneNotSupportedException을 throw하도록 한다.

 

## 3. clone 메서드 재정의시, 주의할 점 !

### 1. 기본적인 clone 메서드 재정의

class PhoneNumber implements Cloneable {

	@Override
	public PhoneNumber clone() {
		try {
			return (PhoneNumber) super.clone();
		} catch(ClassNotSupportedException e) {
		// 아무처리를 하지 않거나, RuntimeException 로 감싸는 것이 사용하기 편하다.
		}
	}
}

 

- super.clone()을 실행하면 PhoneNumber에 대한 완벽한 복제가 이루어진다.

- super.clone()의 리턴타입은 Object이지만, 자바의 공변 변환 타이핑 기능을 이용해 PhoneNumber 타입으로 캐스팅하여 리턴하는 것이 가능하다.

- try-catch 부분으로 감싼 것은 super.clone() 메서드에서 ClassNotSupportedException이라는 checked exception을 리턴하기 때문에 처리해주었다. 하지만 PhoneNumbe가 Cloneable을 구현하기 때문에 절대 실패하지 않는다. 따라서 이부분은 RuntimeException으로 처리하거나, 아무것도 설정하지 않아야 한다.

 

### 2. 가변 상태를 갖는 필드에 대한 복제

public class Stack implements Cloneable{
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;

	public Stack() {
		this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}

	public void push(Object o) {}
}
...

	@Override
	public Stack clone() {
		try {
			Stack result = (Stack) super.clone();
			result.elements =
		} catch(CloneNotSupportedException e) {
		}
	}
}

 

- 단순히 clone() 메서드를 이용해 super.clone()만 실행하게 된다면, new Stack()을 통해 새로운 객체가 생성되고 필드 모두 원본 객체와 동일하게 초기화가 될 것이다. 하지만, Object의 clone 기본 규약에는 deep copy가 아닌 shallow copy를 이요해 초기화를 진행한다고 적혀있다. 따라서, 배열과 같은 가변필드는 원본 필드와 객체를 공유하게 된다.

- clone() 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. 그렇기 때문에 제대로 복제하기 위해서는 elements라는 배열을 똑같이 복사해서 만들어줘야 한다.

 

### 3. 배열 복사

- 배열을 복제하는 방법 중 가장 권장하는 방법은 array.clone()을 이용해 복사하는 방법이다. 사실, 배열은 clone기능을 가장 제대로 사용하는 유일한 예이다.

하지만, array 필드가 final이 적용되어 있다면 array.clone()을 통해 초기화를 할 수 없다. (final이기 때문에 객체 생성이후 초기화 불가)

- Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라 라는 일반 용법과 충돌한다. 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야할 수도 있다.

 

### 4. stack-overflow 문제

public class HashTable implements Cloneable {
	private Entry[] buckets = ...;
	private static class Entry {
		final Object key;
		Object value;
		Entry next;

		Entry(Object key, Object value, Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}
	}

	@Override
	public HashTable clone() {
		try {
			HashTable result = (HashTable) super.clone();
			result.buckets = buckets.clone();
			return result;
		} catch(CloneNotSupportedException e) {
			throw new Assertion();
		}
	}
}

- 복제본은 자신만의 버킷 배열은 갖지만, 배열 내의 Entry는 원본과 같은 연결리스트를 참조하여, 불변성이 깨지게된다.

- 그래서 HashTable.Entry 클래스는 내부적으로 deep copy를 지원하도록 보강되었다. 연결리스트에 대한 next를 복제할 때 재귀적으로 clone을 호출하도록 되어있는데, 재귀호출 때문에 연결리스트의 size만큼 stack frame을 소비하여, 리스트가 길면 stack-overflow 에러를 발생시킬 위험이 있다.

- 이 문제를 해결하기 위해 재귀 호출을 통한 deep copy 대신 반복자를 써서 순회하는 방법으로 수정해야 한다.

 

### 5. 생성자 내에서는 재정의될 수 있는 메서드를 호출하지 말자.

- 만약 clone이 하위 클래스에서 재정의한 메서드를 호출하면, 하위 클래스는 복사과정에서 자신의 상태를 교정할 기회를 잃게 되어 원본과 복제본의 상태가 달라질 수 있다.

 

### 6. 스레드 안전성을 고려한다면 적절히 동기화해야 한다.

- 스레드 안정성을 고려한가면 clone() 메서드에 대해 적절히 동기화 처리를 해야한다.

@Override
public synchronized Object clone() {
	try {
		Object result = super.clone();
	} catch(CloneNotSupportedException e) {
	}
}

 

## 4. clone() 재정의 방법

1. Cloneable을 구현하는 모든 클래스는 clone()을 재정의해야 한다.

2. 접근 제한자는 public으로

3. 반환타입은 클래스 자신으로

4. 가장 먼저 super.clone()을 호출한 후 필요한 필드를 적절히 수정한다.

5. 이후 깊은 구조까지 클론한다. (보통 재귀지만, 항상 정답은 아니다.)

6. 기본 타입 필드, 불변 객체 참조만 갖는 클래스면 아무 필드도 수정할 필요가 없다.

7. 고유 ID는 비록 기본 타입, 불변이어도 수정해야 한다.

 

## 5. clone()재정의 대체법

* 복사 생성자: 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자

public Yum(Yum yum) {...}

 

* 복사 팩터리

public static Yum newInstance(Yum yum) {...}

* 복사 생성자와 복사 팩터리의 장점

1. 위험한 객체 생성 매커니즘을 사용하지 않는다. (생성자없이 객체 생성)

2. 문서화된 규약에 기대지 않는다.

3. final 필드 용법과 충돌하지 않는다.

4. 불필요한 검사 예외를 던지지 않는다.

5. 형변환 필요가 없다.

6. 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다.

- 인터페이스 기반 복사 생성자 = 변환 생성자 (conversion constructor)

- 인터페이스 기반 복사 팩터리 = 변환 팩터리 (conversion factory)

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# toString을 항상 재정의하라

- Object.toString() 메서드: [클래스이름]@[16진수로 표시한 해시코드] 포맷을 갖는다.

- toString의 일반 규약에 따르면, 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.

- toString의 규약은 모든 하위 클래스에서 이 메서드를 재정의하라 라고 하고 있다.

 

## 1. toString을 재정의해야 하는 이유

- toString을 잘 구현한 클래스는 사용하기 편하고, 그 클래스에 대해 디버깅하기 쉽다.

- map 객체를 출력하는 경우 {Jenny=PhoneNumber@addbb}보다는 {Jenny=707-867-5308}이라는 메시지가 가독성이 좋다.

- 실전에서는 toString 메서드를 재작성할 때, 그 객체가 가진 주요 정보를 모두 반환하는 것이 좋다.

- toString을 구현할 때면 반환값의 포맷을 문서화할 지 정해야 한다.

- 값 클래스는 문서화를 권한다.

- 포맷을 명시하면, 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다.

- 포맷을 명시하기로 했으면, 명시한 포맷에 맞는 문자열과 객체를 상호전환할 수 있는 정적 팩터리나 생성자를 함께 제공하면 좋다.

- 단, 포맷을 한번 명시하면, 평생 그 포맷에 얽매이게 된다.

- 포맷을 명시하지 않는다면 다음 릴리즈에 포맷을 변경할 수 있는 유연성을 더 가져갈 수 있다.

- 포맷을 명시하든 아니든, 개발자의 의도는 명확히 밝혀야 한다.

- toString이 반환한 값에 대해 포함한 정보를 얻어올 수 있는 API를 제공하자

- toString에 있는 getter를 제공하지 않는다면, 클라이언트에서 toString을 파싱하여 사용할지도 모른다.

 

포맷을 명시하기로 했으면, 명시한 포맷에 맞는 문자열과 객체를 상호전환할 수 있는 정적 팩터리나 생성자를 함께 제공하면 좋다.

 

## 2. toString을 따로 재정의하지 않아도 되는 경우

- 정적 Utils 클래스는 따로 재정의하지 않아도 된다. (객체의 상태(state)를 가지는 클래스가 아니기 때문이다.)

- enum 타입 또한 이미 완벽한 toString을 제공한다.

- 대다수의 컬렉션 구현체는 추상 컬렉션 클래스(AbstractMap, AbstractSet 등)의 toString 메서드를 상속하여 쓴다.

- 라이브러리를 통해 자동생성

- Google의 @Autovalue

- Lombok의 @ToString

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# equals를 재정의하려거든 hashCode도 재정의하라.

- equals를 재정의한 클래스 모두에서 hasCode도 재정의해야 한다.

일반 규약을 어기게 되어 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생할 수 있다.

 

## 1. Object 명세의 3가지 규약

1. equals 비교에 사용되는 정보가 변경되지 않았다면 객체의 hashCode를 몇 번을 호출해도 항상 같은 값을 반환해야 한다.

2. equals(object)를 통해 두 객체를 비교했을 때 객체가 같다고 판단했다면(true를 반환) 두 객체의 hashCode는 같다.

3. equals(object)가 두 객체를 다르게 판단했다 하더라도 (false를 반환) hashCode가 다를 필요는 없다. (Hash Collision) 단, 다른 객체에 대해서는 다른 값을 반환해야 해시 테이블의 성능이 좋아진다.

 

논리적으로 같은 객체는 같은 hasCode를 반환해야 한다.

 

## 2. hashcode의 동작방법

- 좋은 해쉬 함수라면 서로 다른 인스턴스에 다른 해쉬 코드를 반환한다.

- 주어진 인스턴스들을 균일하게 분배해야 한다. (32비트 정수 범위 내에서)

- 만약 같은 값을 반환한다면, 객체가 해시 테이블 버킷 하나에 담기고, 그 객체들이 연결 리스트처럼 동작한다.

 

## 3. 좋은 hashCode를 작성하는 방법

@Override
public int hashCode() {
	int c = 31;
    
	// 1. int 변수 result를 선언한 후 첫번째 핵심 필드에 대한 hashCode로 초기화한다.
	int result = Integer.hashCode(firstNumber);

	// 2. 기본타입 필드라면 Type.hashCode()를 실핸한다.
	// Type은 기본타입의 Boxing 클래스이다.
	result = c * result + Integer.hashCode(secondNumber);

	// 3. 참조타입이라면 참조타입에 대한 hashCode함수를 호출한다.
	// 4. 값이 null이면 0을 더해준다.
	result = c * result + address == null ? 0 : address.hashCode();

	// 5. 필드가 배열이라면 핵심 원소를 각각 필드처럼 다룬다.
	for(String elem : arr) {
		result += c * result + elem == null ? 0 : elem.hashCode();
	}

	// 6. 배열의 모든 원소가 핵심필드면 Arrays.hashCode를 이용한다.
	result = c * result + Arrays.hashCode(arr);

	// 7. result = 31 * result + c 형태로 초기화하여 result 를 리턴한다.
	return result;
}

- hashCode를 구현했다면 이 메서드가 동치인 인스턴스에 대해 똑같은 해시 코드를 반환할지 Testcase를 작성하자.

-파생 필드는 hashCode 계산에서 제외해도 된다.

- equals 비교에 사용되지 않은 필드는 반드시 제외한다.

- 31 * result를 곱하는 순서에 따라 result 값이 달라진다.

- 곱하는 숫자 31인 이뉴는 31이 홀수이면서 소수(prime)이기 때문이다.

- 31을 이용하면 (i << 5) - i와 같이 최적화할 수 있다.

 

* hashCode를 편하게 만들어주는 모듈

- Objects.hash()

- 내부적으로 auto boxing이 일어나 성능이 떨어진다.

- 입력 인수를 담기 위한 배열 생성으로 속도가 더 느리다.

- Lombok의 @EqualsAndHashCode

- Google의 @AutoValue

 

## 4. hashCode를 재정의할 때 주의할 점 !

- 불변 객체에 대해서는 hashCode 생성 비용이 많이 든다면, hashCode를 캐싱하는 것도 고려하자.

- 스레드 안전성까지 고려해야 한다.

- 성능을 높이고자 hashCode를 계산할 때 핵심 필드를 생략해서는 안된다.

- 속도는 빨라지겠지만, hash 품질이 나빠져서 해시 테이블의 성능을 떨어뜨릴 수 있다. (Hashing Collision)

- hashCode 생성 규칙을 API 사용자에게 공표하지 말자.

- 그래야 클라이언트가 hashCode 값에 의지한 코드를 짜지 않는다.

- 다음 릴리즈 시, 생성을 개선할 여지가 있다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !