[Deep in java] 자바 기본 지식 - 2

이재훈·2023년 11월 14일
0

DEEPINJAVA

목록 보기
3/4

사내에서 진행하는 자바 스터디 3주차 주제입니다.

  • 깊은 복사 vs 얕은 복사
  • 추상 클래스 vs 인터페이스
  • final, static, static final
  • overloading vs overwriting
  • 제네릭 (Generic)

깊은 복사 vs 얕은 복사

깊은 복사 : 실제 값을 새로운 메모리 공간에 복사
얕은 복사 : 주소값을 복사

얕은 복사

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        List<String> shallowCopyList = list;

        list.add("text");

        System.out.println(shallowCopyList.size()); // 1
    }
}

얕은 복사는 주소 값을 복사하기 때문에 참조하고 있는 실제 값이 같습니다.
list 에 데이터를 추가하면 shallowCopyList에 사이즈가 1이 된 것을 확인할 수 있습니다.

깊은 복사

그렇다면 실제 값을 새로운 메모리 공간에 복사는 어떻게 할 수 있을까요?
여러가지 방법이 있습니다.

  • Cloneable 인터페이스 구현
  • 복사 생성자
  • 복사 팩토리 등

What is cloneable

package java.lang;

/**
 * A class implements the {@code Cloneable} 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} interface results in the exception
 * {@code CloneNotSupportedException} 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.
 *
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   1.0
 */
public interface Cloneable {
}

Cloneable 인터페이스 입니다. 위의 설명을 해석해보자면 아래와 같습니다.

클래스는 Cloneable 인터페이스를 구현하여 Object.clone() 메서드에 해당 메서드가
해당 클래스의 인스턴스를 필드 단위로 복사하는 것이 합법임을 나타냅니다.
Cloneable 인터페이스를 구현하지 않은 인스턴스에서 개체의 클론 메서드를 호출하면
CloneNotSupportedException 예외가 생성됩니다.

관례적으로 이 인터페이스를 구현하는 클래스는 공개 메서드로 Object.clone(보호됨)을
재정의해야 합니다. 이 메서드를 재정의하는 방법에 대한 자세한 내용은
Object.clone()을 참조하십시오.

이 인터페이스에는 클론 메소드가 포함되어 있지 않다는 점에 유의한다. 따라서 단순히
이 인터페이스를 구현한다는 사실만으로 개체를 복제할 수는 없다. 클론 메소드가 반사적
으로 호출된다 하더라도 성공한다는 보장은 없다.

이펙티브 자바의 저자 조슈아 블로크는 clone() 메서드의 재정의를 주의해서 진행하라고 했습니다.
결론은 배열을 제외하고는 생성자와 팩토리를 이용하는 방식이 더 낫다는 것입니다.

@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;

clone 메서드는 자바 코드로 작성되어있지 않기 때문에 내부의 코드는 확인할 수 없습니다. 결론적으로 새로운 메모리 주소에 객체를 복제한다 라고 생각하면 됩니다.

예제 코드입니다.

@Setter
@Getter
public class User implements Cloneable{

    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected User clone() throws CloneNotSupportedException {
        return (User)super.clone();
    }
}

복사 생성자

@Getter
@Setter
public class User {
    String name;
    String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
	// 복사 생성자
    public User(User user) { 
        this.name = user.name;
        this.email = user.email;
    }
}

사용법

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

        User user1 = new User("name", "email");
        User user2 = new User(user1);

        System.out.println(user1 == user2); // false
    }
}

복사 팩토리

@Getter
@Setter
public class User {
    String name;
    String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    private User(User user) {
        this.name = user.name;
        this.email = user.email;
    }
	
    // 복사 팩토리
    public static User newInstance(User user) {
        return new User(user);
    }
}

사용법

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

        User user1 = new User("name", "email");
        User user2 = User.newInstance(user1);

        System.out.println(user1 == user2); // false
    }
}

추상 클래스 vs 인터페이스

https://velog.io/@jay_be/JAVA-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4

final, static, static final

static

  • 멤버(변수 또는 메서드)로 선언되면 'static' 클래스의 특정 인스턴스가 아닌 클래스에 속한다는 의미입니다.

  • 'static' 멤버는 클래스 인스턴스 수에 관계없이 전체 클래스에 대한 변수 또는 메서드를 공유합니다.

  • 'static' 멤버는 클래스 인스턴스를 통하지 않고 클래스 이름을 사용하여 액세스 합니다.

예시

public class StaticEx {
    public static int level = 0;

    public void levelUp() {
        level += 1;
    }

    public void levelDown() {
        level -= 1;
    }
}
public class TestMain {
    public static void main(String[] args) {

        StaticEx staticEx1 = new StaticEx();
        StaticEx staticEx2 = new StaticEx();
        
        staticEx1.levelUp();

        System.out.println(StaticEx.level); // 1

        staticEx2.levelDown();

        System.out.println(StaticEx.level); // 0
    }
}
  • 상수 : 값이 클래스의 모든 인스턴스 간에 공유되고 변경되지 않을 것으로 예상되는 경우 사용합니다.
public class Util {
    
    public static int plus10(int number) {
        return number + 10;
    }
}
  • 유틸리티 메서드 : 인스턴스별 데이터에 의존하지 않고 클래스의 모든 인스턴스에서 공유 할 수 있는 메서드입니다.

final

  • final 변수를 선언하면 값이 할당된 후에 값을 변경할 수 없습니다. 메서드의 경우 final 하위 클래스가 메서드를 재정의할 수 없습니다.
  • 클래스의 경우 final 클래스를 상속 받을 수 없습니다.

예시

public final class FinalEx { // 해당 클래스 상속 불가능
    public final int finalValue = 10; // 해당 멤버변수 변경 불가능
    
    public void saveUser(final User user) { // 변경되지 않는 매개변수
        // user 저장 로직
    }
}

static final

  • 변수에 사용하면 해당 변수는 모든 인스턴스에서 공유되는 상수라는 의미입니다.
  • 프로그램 수명 동안 변경되지 않는 상수에 자주 사용됩니다.
public class FinalEx{
    public static final double PI = 3.14;
}

정리

  • static : 멤버가 클래스의 모든 인스턴스에서 공유되고 인스턴스별 데이터에 의존하지 않는 경우에 사용됩니다.

  • final : 상수를 선언하려는 경우, 메서드, 변수가 수정되는 것을 방지하려는 경우에 사용됩니다.

  • static final : 클래스의 모든 인스턴스에서 공유되고 변경되지 않는 상수에 사용됩니다.

  • 정적 변수는 클래스가 로드될 때 JVM 메서드 영역에 할당됩니다. 모든 인스턴스는 동일한 정적 변수를 공유합니다.

overloading vs overwriting

overloading

하는 일은 같지만 매개변수 갯수 또는 타입이 다를 때, 같은 이름을 사용해서 메서드를 정의할 수 있다. 대표적인 예시로 java.lang 패키지에 println 메서드가 있습니다.


    /**
     * Prints a character and then terminate the line.  This method behaves as
     * though it invokes {@link #print(char)} and then
     * {@link #println()}.
     *
     * @param x  The {@code char} to be printed.
     */
    public void println(char x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints an integer and then terminate the line.  This method behaves as
     * though it invokes {@link #print(int)} and then
     * {@link #println()}.
     *
     * @param x  The {@code int} to be printed.
     */
    public void println(int x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints a long and then terminate the line.  This method behaves as
     * though it invokes {@link #print(long)} and then
     * {@link #println()}.
     *
     * @param x  a The {@code long} to be printed.
     */
    public void println(long x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints a float and then terminate the line.  This method behaves as
     * though it invokes {@link #print(float)} and then
     * {@link #println()}.
     *
     * @param x  The {@code float} to be printed.
     */
    public void println(float x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints a double and then terminate the line.  This method behaves as
     * though it invokes {@link #print(double)} and then
     * {@link #println()}.
     *
     * @param x  The {@code double} to be printed.
     */
    public void println(double x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints an array of characters and then terminate the line.  This method
     * behaves as though it invokes {@link #print(char[])} and
     * then {@link #println()}.
     *
     * @param x  an array of chars to print.
     */
    public void println(char[] x) {
        if (getClass() == PrintStream.class) {
            writeln(x);
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints a String and then terminate the line.  This method behaves as
     * though it invokes {@link #print(String)} and then
     * {@link #println()}.
     *
     * @param x  The {@code String} to be printed.
     */
    public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

    /**
     * Prints an Object and then terminate the line.  This method calls
     * at first String.valueOf(x) to get the printed object's string value,
     * then behaves as
     * though it invokes {@link #print(String)} and then
     * {@link #println()}.
     *
     * @param x  The {@code Object} to be printed.
     */
    public void println(Object x) {
        String s = String.valueOf(x);
        if (getClass() == PrintStream.class) {
            // need to apply String.valueOf again since first invocation
            // might return null
            writeln(String.valueOf(s));
        } else {
            synchronized (this) {
                print(s);
                newLine();
            }
        }
    }
  • 리턴값만 다르게 지정하는 것은 오버로딩이 아닙니다. (컴파일 에러 발생)
  • 접근 제어자도 자유롭게 지정할 수 있습니다. (public, default, protected, private)
    but 접근제어자만 다르다고 오버로딩이 가능한 것은 아닙니다.
  • 오버로딩은 매개변수의 차이로만 구현이 가능합니다.

오버로딩을 사용하는 이유

  1. 같은 기능을 하는 메서드를 하나의 이름으로 사용할 수 있다.
  2. 메서드 이름을 절약할 수 있다.

overwriting

부모 클래스에서 상속받은 메서드를 자식 클래스에서 재정의 하는 것을 오버라이딩이라고 합니다. 상속 받은 메서드를 그대로 사용할 수도 있지만, 자식 클래스의 상황에 맞게 변경해야하는 경우 오버라이딩을 사용하여 사용합니다.

오버라이딩은 부모 클래스의 메서드를 재정의 하는 것이기 때문에, 자식 클래스에서는 오버라이딩 하고자 하는 메서드의 이름, 매개변수, 리턴 값이 모두 같아야 합니다.

에시

public class Parent {
    
    public void running() {
        System.out.println("parent running");
    }
}
public class Child extends Parent{

    @Override
    public void running() {
        System.out.println("child running");
    }
}

@Override는 필수일까?

Override 어노테이션의 역할은 컴파일 시점에서 부모의 메서드를 문법에 맞게 오버라이딩 했는지 검사을 하는 역할을 합니다. 없더라도 정상 동작을 합니다.

public class Child extends Parent{

    public void running() {
        System.out.println("child running");
    }
}

오버라이딩 규칙

  1. 자식 클래스에서 오버라이딩하는 메서드의 접근 제어자는 부모 클래스보다 더 좁게 설정할 수 없다.
  2. static 메서드를 인스턴스 메서드 또는 반대로 바꿀 수 없다.

제네릭 (Generic)

  • 컴파일 타임 유형 안전성을 제공
  • 다양한 유형의 객체에 대해 작동하는 클래스, 인터페이스 및 메서드를 생성하는 방법 제공
  • Java5에 도입된 기능

제네릭을 사용하면 컴파일 시점에 타입이 확인되므로 타입 안정성을 검증하고, 다양한 타입에서 작동할 수 있는 코드를 작성할 수 있습니다.

public class Box<T>{
    
    private T value;
    
    public void setValue(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
}

여기 어떤 타입이 들어가는지 모르는 Box 클래스가 있습니다.

public class BoxEx {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        intBox.setValue(2);
        System.out.println(intBox.getValue()); // 2

        Box<String> strBox = new Box<>();
        strBox.setValue("apple");
        System.out.println(strBox.getValue()); // apple
    }
}

객체를 생성할 때 타입을 넣어주면 해당 타입에 맞게 Box를 사용할 수 있습니다.
만약 제네릭이 없었다면 Box 클래스를 두개를 만들던가, Object 타입으로 만들어야 합니다.

보통 제네릭은 아래 표의 타입이 많이 사용됩니다.

제네릭 메서드를 선언하는 방법입니다.

public <T> T someMethod(T o) {

}

[접근제어자] <제네릭타입> [반환타입] [메서드명] ([제네릭 타입] [파라미터]) {

}

제한된 Generic, 와일드 카드

지금까지 제네릭은 참조 타입 모두가 될 수 있었습니다. 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 할까요?

extends

<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정됨)
<? extends T> // T와 T의 자손 타입만 가능

이 둘의 차이는 들어온 타입의 데이터를 조작하고자 하는 경우 위, 아닐 경우 아래를 사용합니다.

super

<K super T> // T와 T의 부모 타입만 가능 (K는 들어오는 타입으로 지정됨)
<? super T> // T와 T의 부모 타입만 가능

클래스를 정의할 때 보통 사용되지는 않습니다. super는 보통 매개변수에서 와일드 카드의 범위를 정의할 때 사용합니다.

public class ExampleClass<T> {
    
    public void processElement(List<? super T> list, T element) {
    
        list.add(element);
    }
}

<?>

?는 어떤 타입이든 상관 없다는 의미입니다. (? extends Object) 와 동일


참조 블로그
https://catsbi.oopy.io/16109e87-3c7e-4c6e-9816-c86e6b343cdd

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

https://hyoje420.tistory.com/14

https://st-lab.tistory.com/153

profile
부족함을 인정하고 노력하자

0개의 댓글