Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

forDevLife

[생활코딩-자바입문] 핵심 요약 6 본문

Java

[생활코딩-자바입문] 핵심 요약 6

JH_Lucid 2020. 11. 15. 20:16

생활코딩 42. 참조, 43. 제네릭

 

1) 복제란?

 

데이터 타입을 생성할 때, new를 통해 생성하는 것들은 기본 데이터 타입이 아니고 참조형 또는 참조 데이터 타입이라 한다.

참조에 앞서, 복제라는 개념에 대해서 먼저 보자.

 

아래에서, 클래스 A를 만들었다. runValue 메소드 내에서는 데이터 타입을 int로 하였으며, runReference 메소드 내에서는 데이터 타입을 new를 통한 인스턴스로 생성하였다.

class A {
	public int id;
	
	A(int id) {
		this.id = id;
	}
}

public class ConstantDemo {
	public static void runValue() {
		int a = 1;
		int b = a;
		b = 2;
		System.out.println("runValue, "+a);
	}
	
	public static void runReference() {
		A a = new A(1);
		A b = a;
		b.id = 2;
		System.out.println("runReference, "+a.id);
	}
	
	public static void main(String[] args) {
		runValue();
		runReference();
	}
}

결과적으로, runValue를 통한 출력은 a의 원래 값에 영향을 주지 않았다. int b = a에서 그저 값의 복사만 이루어 졌다.

반면, new를 통해 생성된 a를 b에 넣을때에는 원래 a의 id도 2로 변경되어 버렸다.

 

A a = new A(1)가 실행되면 A 클래스의 인스턴스 변수가 저장되는게 아니고, 일단 클래스의 인스턴스가 메모리 어딘가에 만들어진다. 그리고 변수 a는 이 인스턴스의 위치 정보를 담고 있다. 이 상태에서 A b = a;를 실행하면 b 변수에 A 인스턴스가 새로 들어가는게 아니고, a 변수가 가리키고 있는 A 인스턴스에 대한 주솟값(위치 정보)만 복제된다. 따라서 b.id = 2를 통해 처음에 할당받은 인스턴스의 값이 변경됨을 알 수 있다. 이를 참조(reference)라 한다.

b.id = 2의 결과로, a.id도 동일하게 바뀌었다.

 

2) 참조와 복제

 

쉽게 생각해서, 윈도우 창에서 파일을 복사 누르는 건 복제이고, 바로가기를 만드는건 참조이다. 

기본 데이터 타입이 지정되어 있다면, 각 변수가 기본 데이터 타입의 데이터를 직접 갖고 있다는 것이고 기본 데이터 타입이 아닌 변수(new를 이용해 만들어진 타입)를 만들었다는 것은 인스턴스를 참조하고 있다는 뜻이다.

자바가 이처럼 동작하는 것은 참조가 메모리를 훨씬 적게 사용하고, 원본의 변화가 모든 참조 데이터형에 반영된다는 특성을 사용한 결과이다. 

 

 

3) 메소드의 매개변수와 참조, 복제

 

아래는 별거 아닌데 그냥 봐보자.

public class ReferenceParameter {
	static void _value(int b) {
		b = 2;
	}
	
	public static void runValue() {
		int a = 1;
		_value(a);
		System.out.println("runValue, " +a);
	}
	
	static void _reference1(A b) {
		b = new A(2);
	}
	
	public static void runReference1() {
		A a = new A(1);
		_reference1(a);
		System.out.println("runReference1, " +a.id);
	}
	
	static void _reference2(A b) {
		b.id = 2;
	}
	
	public static void runReference2() {
		A a = new A(1);
		_reference2(a);
		System.out.println("runReference2, " +a.id);
	}
	
	public static void main(String[] args) {
		runValue();
		runReference1();
		runReference2();
	}
}

value / reference1 / reference2에 대해 각각 보면,

 

value : _value 메소드를 보면, int b라는 인자에 a의 값을 복사하는 것이다. a 변수는 기본 데이터 타입이라 이후에 b를 변경해도 바뀌지 않는다. 따라서 a.id 출력은 1.

 

reference1 : a변수에 인스턴스의 주소를 담은 후, b에 해당 주소를 전달한다. 하지만 이 안에서 b = new A(2)를 통해 새로운 인스턴스를 만들어 참조하므로, 이후에 b의 값이 바뀌어도 영향 주지 않는다. 따라서 a.id 출력은 1.

 

reference2 : ref1과 동일하나, 전달 후에 b를 이용하여 a와 동일하게 가리키고 있는 인스턴스의 id를 바뀐다. 따라서 a.id 출력은 2.

 

 


 

1) 제네릭의 사용법

 

제네릭은 한국어로 '포괄적인', '일반적인'을 의미한다. 제네릭은 클래스 내부에서 사용할 데이터 타입을 인스턴스를 생성할 때 확정하는 것을 말한다. 아래 그림을 보자. (이건 마치 c의 템플릿 같은 느낌..!)

즉 아래와 같이, class를 사용할 때 변수의 type을 지정해 줄 수 있는 것이다. 2, 3번째 줄 처럼 꺽쇠 안에 type을 지정해 둔 후, 인스턴스를 생성하면 된다. 중복 코드를 없애기 위해서 사용하는데 사용 예시는 아래서 확인하자.

 

 

 

아래는 generic 사용 예시이다.

Person을 generic으로 만들었고, T, S는 와일드카드라 명명한다. 

main에서 Person<EmployeeInfo, Integer>를 통해 info와 id의 데이터 타입을 지정하였다.

 

- S id의 경우 Integer라는 것으로 대체되었다. 이는 래퍼 클래스라고 하는데, 기본 데이터 타입(int 등의)을 마치 객체처럼 다룰 수 있도록 하는 클래스이다. 제네릭에서 T나 S에 오는 데이터 타입은 참조형만 가능하기 때문에 래퍼클래스를 사용하여 기본 데이터 타입도 참조할 수 있도록 해 주는 것이다.

+ JAVA 9부터는, Integer i = new Integer(1); 과 같은 방식을 선호하지 않는다.  Integer i = 1이라고 해주면 된다. 직접 대입도 가능

+ intValue 메소드를 통해 id 값을 확인한다. 이는 Integer가 가진 원래 숫자를 원시 데이터 타입으로 변환하는 메소드이다. 하지만 걍 p1.id로 접근해도 상관 없는 것으로 출력되긴 하다. 

class EmployeeInfo {
	public int rank;
	
	EmployeeInfo(int rank) {
		this.rank = rank;
	}
}

class Person<T, S> {
	public T info;
	public S id;
	
	Person(T info, S id) {
		this.info = info;
		this.id = id;
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		EmployeeInfo e = new EmployeeInfo(1);
		Integer i = 1;
		Person<EmployeeInfo, Integer> p1 =
				new Person<EmployeeInfo, Integer>(e, i);
		int a = i; // a에 Integer가 대입 가능한지 확인하기 위함
		System.out.println(a + 2);  // Integer가 정상적으로 산술되는걸 확인하기 위함
		System.out.println(p1.id.intValue());
		System.out.println(p1.info.rank);
	}
}

 

2) 제네릭의 특성

 

이번에는 제네릭을 생략하는 방법에 대해서 알아보겠다.

아래는 p1의 경우 정석대로 파라미터 작성, p2는 생략하였다. 어차피 자바 입장에서 첫번째 파라미터와 두번째 파라미터에 전달되는 타입을 추측할 수 있으므로 제네릭을 언급하지 않아도 된다고 한다.

하지만 생략했을 경우 출력이 되긴 하나 노란줄이 그어진다. 또한 rank의 경우 접근이 불가능하다. 이유는? 모르겠다. 일단 정석대로 한다.

 

+ 이 부분은 맨 아래서 해결 된다. 제네릭에 extends를 통해 특정을 해 줘야 해당 인자로의 접근이 가능하며, 이는 파라미터를 생략했을 경우에만 해당한다. 따라서 걍 일단 파라미터는 무조건 쓰던지, 아니면 extends 시 특정을 하던지 해야겠다.

 

 

 

이번에는 제네릭을 메소드 수준에서 사용해본다.

printInfo 메소드에 U 제네릭을 통해, info를 출력하기 위한 메소드를 작성하였다. 아래 printinfo사용 시, 데이터 타입을 지정하여 해당 메소드를 호출할 수 있다. 결과는 뭐 클래스에 대한 정보와 hex 값이 나올거라 예상할 수 있다.

class Person<T, S> {
	public T info;
	public S id;
	
	Person(T info, S id) {
		this.info = info;
		this.id = id;
	}
	
	public <U> void printInfo(U info) {
		System.out.println(info);
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		EmployeeInfo e = new EmployeeInfo(1);
		Integer i = 10;
		Person <EmployeeInfo, Integer> p1 =
				new Person <EmployeeInfo, Integer> (e, i);	
		p1.<EmployeeInfo>printInfo(e);
		p1.printInfo(e); // 제네릭 생략 가능
	}
}

 

3) 제네릭의 제한

 

제네릭을 통해 오만가지 잡동사니가 들어갈 가능성이 있다. 이거에 대해 제한을 두는 방법은, 아래와 같다.

EmployeeInfo 클래스의 부모클래스 Info를 만들고, getlevel이라는 메소드를 하위 클래스에서 구현하도록 abstract로 강제하였다.

Person에 T 제네릭을 받을건데, 들어오는 데이터 타입을 Info 또는 그 자식으로 받고 싶다고 정의하였다. 따라서 main의 두 번째 string으로 들어오는 부분은 컴파일 단계에서 에러가 발생한다.

abstract class Info {
	public abstract int getLevel();
}

class EmployeeInfo extends Info {
	public int rank;
	
	EmployeeInfo(int rank) {
		this.rank = rank;
	}
	public int getLevel() {
		return this.rank;
	}
}

class Person<T extends Info> {
	public T info;
	
	Person(T info) {
		this.info = info;
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
		Person<String> p2 = new Person<String>("부장"); // 이 부분은 오류 발생
	}
}

 

또한 제네릭의 extends에는 인터페이스를 사용해도 된다. extends는 제네릭의 맥락에서 사용될 때는 상속 의미가 아닌 '부모가 누구다'라는 것을 의미하기 때문이다. 따라서 EmployeeInfo 부분만 extends -> implements로 바꾼다. 인터페이스는 하위클래스가 인터페이스의 항목을 반드시 갖도록 강제하므로, Person 입장에선 Info를 상속하는 클래스만을 제네릭으로 받을 수 있다는 의미를 전달한다.

interface Info {
	int getLevel();
}

class EmployeeInfo implements Info {
	public int rank;
	
	EmployeeInfo(int rank) {
		this.rank = rank;
	}
	public int getLevel() {
		return this.rank;
	}
}

class Person<T extends Info> {
	public T info;
	
	Person(T info) {
		this.info = info;
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(100));
		System.out.println(p1.info.getLevel());
	}
}

 

 

마지막은, 위에서 해결되지 않았던 부분이다.

getLevel을 쓰기 위해서는, 제네릭 부분을 extends를 통해 특정해줘야 한다. 특정하지 않을 경우 이는 마치 <T extends Object>와 동일하기 때문이다. Object가 가진 메소드는 toString, clone 등의 공통 메소드이므로, 그 밖의 메소드는 제네릭으로 선언한 객체에서는 호출할 수 없다. extends 뒤에 Info를 지정하면 Info가 가진 메소드를 사용할 수 있다.

 

위에서 말한 것처럼, 부모가 Object임(<T extends Object>) 이라고만 하는건 공통 메소드만 사용한다고 하는 무책임? 한걸로 보이므로 좀 더 구체화해야 한다고 생각합시다.

 

+ 만약 main에서 제네릭 정석대로 파라미터를 특정하면, 이럴 필요 없이 메소드를 사용할 수 있다.

+ 제네릭 파라미터를 생략하게 될 경우에는 extends로 특정지어야 한다.

+ 따라서 일단 귀찮더라도 제네릭 파라미터를 생략하지 말자.

 

interface Info {
	int getLevel();
}

class EmployeeInfo implements Info {
	public int rank;
	
	EmployeeInfo(int rank) {
		this.rank = rank;
	}
	public int getLevel() {
		return this.rank;
	}
}

class Person<T extends Info> { // 무조건 extends로 해 줘야 Info의 메소드인 getLevel을 쓸 수 있다.
	public T info;
	
	Person(T info) {
		this.info = info;
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		Person p1 = new Person(new EmployeeInfo(100));
		System.out.println(p1.info.getLevel());
	}
}

 

 

Comments