이펙티브자바 정리 3

이봐요이상해씨·2021년 12월 27일
0

목록 보기
3/6

15. 클래스와 멤버의 접근 권한을 최소화하라

정보은닉, 캡슐화의 장점

  1. 개발속도 증진 → 병렬 개발
  2. 관리비용 낮춤 → 디버깅, 컴포넌트 파악 유리
  3. 성능 최적화
  4. 재사용성 증가

→ 모든 클래스와 멤버의 접근성을 가능한 좁혀야 함

클래스 멤버 접근 제한자

  1. private: 멤버를 선언한 톱레벨 클래스에서만 접근 가능
  2. package-private:멤버가 소속된 패키지 안의 모든 클래스 에서 접근가능, 접근 제한자 명시 않았을시 적용되는 기본 수준
  3. protected : package-private의 접근 범위 포함, 이 멤버를 선언한 클래스의 하위클래스에서도 접근 가능
  4. public : 모든곳에서 접근 가능

해당 클래스의 공개 api 하나 설정후, 나머지는 모두 private 처리(package-private)

하지만 serializable 구현시 그 필드는 모두 공개 될 수 있다

상위클래스의 메서드를 재정으 할 때는 그 접근 수준을 상위 클래스보다 좁게 설정할 수 없다 → 리스코프 치환 원칙 위배 되기 때문(상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다)

public 클래스의 인스턴스 필드는 public이 아니여야 한다 → 불변식을 보장할수 없기 때문, 스레드 안전하지 않음

16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 이용해라

package com.springandjava.Test.effectivejava.chapter4;

public class ex16 {
	
	private double x;
	private double y;
	
	public ex16(double x, double y){
		this.x = x;
		this.y = y;
	}

	public double getX() {
		return x;
	}

	public double getY() {
		return y;
	}

	public void setX(double x) {
		this.x = x;
	}

	public void setY(double y) {
		this.y = y;
	}
}

패키지 바깥에서 접근할 수 있는 클래스라면 접근제를 제공해야 한다

17. 변경 가능성을 최소화하라

불변클래스 : 인스턴스 내부 값을 수정할 수 없는 클래스

클래스를 불변으로 만드는 방법

  1. 객체의 상태를 변경하는 메서드를 제공하지 않는다
  2. 클래스를 확장할 수 없도록 한다
  3. 모든 필드를 final로 선언한다
  4. 모든 필드를 private으로 선언한다
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다
package com.springandjava.Test.effectivejava.chapter4.ex17;

public final class Complex {
	private final double re;
	private final double im;

	public Complex(double re, double im){
		this.re = re;
		this.im = im;
	}

	public double realPart(){
		return re;
	}

	public double imaginaryPart(){
		return im;
	}

	public Complex plus(Complex c){
		return new Complex(re + c.re, im + c.im);
	}

	public Complex minus(Complex c){
		return new Complex(re - c.re, im - c.im);
	}

	public Complex times(Complex c){
		return new Complex(re*c.re - im*c.im, re*c.im + im*c.re);
	}

	public Complex dividedBy(Complex c){
		double tmp = c.re * c.re + c.im * c.im;
		return new Complex((re * c.re + im*c.im) / tmp, (im * c.re - re*c.im)/tmp);
	}

	@Override
	public boolean equals(Object o){
		if (o == this)
			return true;
		if (!(o instanceof Complex))
			return false;
		Complex c = (Complex) o;

		return Double.compare(c.re, re) == 0 && Double.compare(c.im, im) == 0;
	}

	@Override
	public int hashCode(){
		return 31 * Double.hashCode(re) + Double.hashCode(im);
	}
	
	@Override
	public String toString(){
		return "(" + re + " + " + im +"i)";
	}

}

plus, minus, times diviedBy → 매서드는 인스턴스 자신을 수정하지 않고 새로운 complex인스턴스를 만들어 반환함

불변 객체는 근본적으로 스레드 안전하기 때문에 따로 동기화할 필요가 없다

→스레드 안전(멀티 스레드 환경에서 한 스레드에 동시 접근해도 문제가 없는 것)

불변 객체는 자유롭게 공유할 수 있고, 내부 데이터를 공유할 수 있다.

불변클래스를 만드는 또다른 방법중 하나 → 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공함

public final class Complex {
	private final double re;
	private final double im;

	// public Complex(double re, double im){
	// 	this.re = re;
	// 	this.im = im;
	// }
	
	private Complex(double re, double im){
		this.re = re;
		this.im = im;
	}
	
	public static Complex valueOf(double re, double im){
		return new Complex(re, im);
	}
  1. 외부에서는 package-private → 클래스 확장이 불가함

정적 팩토리 메소드 : 메서드를 통해서 객체를 생성하는 것을 정적 팩토리 메서드라고 한다.(아이템 1)

18. 상속보다는 컴포지션을 사용하라

상속은 메서드 호출과 달리 캡슐화를 깨트린다

기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자

컴포지션(composition) : 기존 클래스가 새로운 클래스의 구성 요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션 이라고 한다

포워딩(forwarding) : 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환

포워딩메소드(forwarding method) : 새 클래스의 메서드들의 전달 메서드

→ 이렇게 함으로서 기존 클래스에 영향을 받지 않는다.

19. 상속을 고려해 설계하고 문서화 해라 그러지 않았다면 상속을 금지해라

상속용 클래스는 재정의할 수 있는 메소드를 문서로 남겨야 한다

→ @implSpec 태그

주의 할점!!

  1. 상속용 클래스는 배포 전에 반드시하위 클래스를 만들어 검증해야 한다
  2. 상속용 클래스의 생성자는 직접적으로, 간접적으로 재정의 가능한 메서드를 호출해서는 안된다
public class Super{
	public Super(){
		overrideMe();
	}
	public void overrideMe(){
	}
}

package com.springandjava.Test.effectivejava.chapter4.ex19;

import java.time.Instant;

public final class Sub extends Super{
	private final Instant instant;

	Sub(){
		instant = Instant.now();
	}

	@Override
	public void overrideMe(){
		System.out.println(instant);
	}
	public static void main(String[] args){
		Sub sub = new Sub();
		sub.overrideMe();
	}
}

여기서는 overridemer가 재 정의되는데,

하위 클래스 생성자가 인스턴스 필드를 초기화 하기 전에 overrideme를 호출함 따라서 첫번째는 null을 출력

(상위 클래스 생성자는 하위 클래스 생성자가 인스턴스 필드를 초기화 하기전에 미리 호출함) → 즉 초기화가 안된 상태에서 호출하려다보니 nullpointException이 나옴

따라서 상속용 클래스가 아니면 상속하지 못하도록 구현하는게 좋다(final로 선언하거나 생성자 모두를 외부에 접근할수 없도록)

20. 추상클래스보다는 인터페이스를 우선하라

인터페이스는 믹스인 정의에 안성 맞춤이다

믹스인 : 클래스가 구현할 수 있는 타입으로 클래스 원래의 주된 타입외에도 특정 행위를 제공한다고 선언해줄 수 있다. 즉 이와 같이 대상의 메인 타입 외에도 추가로 선택적 기능을 혼합(mixed in)한다고 함

인터페이스로는 걔층구조가 없는 프레임워크를 만들 수 있다. (추상클래스는 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위타입이되어야 하기 때문)

탬플릿 메서드 패턴 : 인터페이스와 골격 구현 클래스를 함께 제공하는 식 → 인터페이스와 추상클래스의 장점을 모두 취하는 방식

static List<Integer> intArrayAsList(int[] a) {
	Objects.requireNonNull(a);
	return new AbstractList<>() {
		@Override
		public Integer get(int i) {
			return a[i];
		}

		@Override public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val; //오토언박싱 autoboxing이란 래퍼(Wrapper) 클래스의 객체로 변환하는 것을 말합니다.
			return oldVal; //오토박싱
		}

		@Override public int size(){
			return a.length;
		}
	}
}

래퍼클래스란 기본 타입의 데이터를 객체로 취급해야 하는 경우가 있는데 기본 타입의 데이터를 그대로 사용할수는 없고 데이터를 객체로 변환해야 하는데 해당하는 데이터들을 객체로 포장해주는 것

21. 인터페이스는 구현하는 쪽을 생각해 설계해라

자바 [JAVA] - 제네릭(Generic)의 이해

자바 collection 인터페이스에 추가된 removeIf 메서드

default boolean removeIf(Predicate<? super E> filter) {
	Objects.requireNonNull(filter);
	boolean result = false;
	for (Iterator<E> it = Iterator(); it.hasNext(); ) {
		if (filter.test(it.next())) {
			it.remove();
			result = true;
		}
	}
	return result;
}

Predicate는 Type T 인자를 받고 boolean을 리턴하는 함수형 인터페이스

|<? super E> 는 부모와 현재 형태의 element만 받는 다는 뜻

반복자를 이용해 순회하면서 predicate를 호출하고, true 반환시 remove 메서드를 호출해 해당 원소 제거

22. 인터페이스는 타입을 정의하는 용도로만 사용해라

상수인터페이스 와 같은 안티패턴 사용 금지

public interface Ex22 {
	static final double A = 22;
	static final double B = 23;
	static final double C = 24;
}

차라리 상수 전용 클래스를 생성해라

public class EX22class {

	//인스턴스화 방지
	private EX22class(){

	}
	public static final double A = 22;
	public static final double B = 23;
	public static final double C = 24;
}

23. 태그 달린 클래스보다 클래스 계층 구조를 활용해라

Tagged 구조

package com.springandjava.Test.effectivejava.chapter4.ex23;

import org.hibernate.AssertionFailure;

public class tagged {
	enum Shape { RECTANGLE, CIRCLE}
	
	final Shape shape;
	
	double length;
	double width;
	
	double radius;
	
	tagged(double radius) {
		shape = Shape.CIRCLE;
		this.radius = radius;
	}
	
	tagged(double length, double width){
		shape = Shape.RECTANGLE;
		this.length = length;
		this.width = width;
	}
	
	double area() {
		switch(shape){
			case RECTANGLE:
				return length * width;
			case CIRCLE:
				return Math.PI * (radius * radius);
			default:
				throw new AssertionError(shape);
		}
	}
}

계층 구조로 작성(추상화클래스를 이용해서)

abstract class hiarchy {
	abstract double area();
}

class Circle extends hiarchy {
	final double radius;
	
	Circle(double radius) {
		this.radius = radius;
	}
	@Override
	double area() {
		return Math.PI * (radius * radius);
	}
}

class Rectangle extends hiarchy{
	final double length;
	final double width;
	
	Rectangle(double length, double width){
		this.length = length;
		this.width = width;
	}
	@Override
	double area(){
		return length*width;
	}
}

25. 멤버 클래스는 되도록 static으로 만들라

중첩 클래스 (nested class) : 클래스 안에 정의된 클래스, 자신을 감싼 바깥 클래스에서만 쓰여야 되며 그외에 사용되면 탑레벨로 빼야함

중첩클래스의 종류

정적 멤버클래스, 멤버 클래스(비정적), 익명클래스, 지역클래스

여기서 정적 멤버클래스를 제외하면 나머지는 모두 내부 클래스이다.

  • 정적 멤버 클래스

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 따라서 this를 사용해서 바깥 인스턴스 메서드를 호출하거나 참조할 수 있다. (클래스명.this)

따라서 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적멤버클래스로 만들어야 한다

비정적 멤버 클래스는 바깥 인스턴스 없이 생성 불가

  • 비정적 멤버 클래스

어뎁터를 정의할때 자주 쓰인다 → 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게하는 뷰로 사용

—> 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어라

  • 익명클래스

선언과 동시에 인스턴스가 만들어진다.

비정적문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다.

여러 인터페이스를 구현할 수 없고, 인터페이스 구현동시에 다른 클래스를 상속할 수 없다.

주로 람다 처리에 사용

  • 지역클래스

비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할수 있고 정적 멤버는 갖을 수 없다

25. 탑레벨 클래스는 한 파일에 하나만 담으라

public class main {
	public static void main(String[] args) {
		System.out.println(Utensil.NAME + Dessert.NAME);
	}
}

class Utensil{
	static final String NAME = "pan";
}

class Dessert{
	static final String NAME = "cake";
}

하지만 이와 같은 파일이 다른 파일에 똑같이 작성되어 있다면?

→ 컴파일러에게 먼저 전달되는 파일로 컴파일 될 것이다

→ 해결법 : 탑레벨 클래스를 서로 분리(utensil, dessert)

public class main {
	public static void main(String[] args) {
		System.out.println(Utensil.NAME + Dessert.NAME);
	}
	private static class Utensil{
		static final String NAME = "pan";
	}

	private static class Dessert{
		static final String NAME = "cake";
	}

}

이렇게 정적 클래스로 바꾸고 한파일에 작성해도 된다

0개의 댓글