Object 타입을 사용한 Box 클래스는 모든 종류의 객체를 저장할 수 있다. Object 클래스가 모든 자바 클래스의 최상위 부모이기 때문에, 모든 객체는 자동으로 Object 타입으로 변환되어 저장될 수 있으나, 이를 읽어올 때는 강제 타입 변환이 필요하다.
Box box = new Box();
box.set("hello"); // String 타입을 Object 타입으로 자동 변환하여 저장
String str = (String) box.get(); // Object 타입을 String 타입으로 강제 변환하여 가져옴
이 방식은 다양한 객체를 저장할 수 있지만, 강제 타입 변환을 해야 하는 불편함이 있다. 잘못된 타입으로 변환할 경우, ClassCastException이 발생할 위험도 있다.
제네릭을 사용하면 타입 안정성을 확보하면서도 타입 변환을 할 필요가 없다.
public class Box<T> {
private T t;
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
}
Box 클래스를 사용할 때, 타입 파라미터 T를 구체적인 타입으로 지정
Box<String> box = new Box<String>();
이 경우, T는 String으로 대체되며, Box 클래스 내부는 자동으로 다음과 같이 재구성
public class Box<String> {
private String t;
public String get() {
return t;
}
public void set(String t) {
this.t = t;
}
}
set() 메소드는 String 타입만 허용하고, get() 메소드는 String 타입을 반환
Box<String> box = new Box<String>();
box.set("hello");
String str = box.get();
Util : 프로젝트에서 자주 사용하는 값을 정적인 값으로 선언하여 외부에서 호출하여 사용하는 방식이 자주 이용된다. (static 변수 = 정적 변수 = 클래스 변수)
public class Util {
public static final double PI = 3.14159;
}
타입 파라미터에 구체적인 타입을 제한하는 기능, 타입을 상속받은 자식들만 올 수 있다. 메소드는 매개값으로 Number타입 혹은 Byte, Short, Integer, Long, Double타입의 인스턴스만 가져야 한다.

Number라는 추상 클래스를 상속받고 있다. → 숫자 정수형을 사용할 수 있게 된다.
제한된 타입 파라미터를 선언하려면 타입 파라미터 뒤에 extends 키워드가 붙고 상위 타입을 명시하면 된다.
public <T extends 상위타입> 리턴타입 메소드(매개변수, ...) {
}
최상위 타입에 Number을 지정해주고 타입 파라미터 를 지정해주면 Number라는 타입을 가지고 있는 클래스들만 매개변수로 올 수 있다(T t1, T t2). 타입을 상속받고 있는 클래스만 오도록 제한을 해주는 것이다. Number 클래스에서 제공해주는 메서드를 사용할 수 있다.
public <T extends Number> int compare(T t1, T t2) {
double v1 = t1.doubleValue();
double v2 = t2.doubleValue();
return Double.compare(v1, v2)
}
compare 메서드와 를 사용하여 입력 받는 값((T t1, T t2))을 제한받고, 입력 받은 값을 비교하는 코드 작성, 비교를 위해 타입을 제한하는 것
타입에 안정성을 가질 수 있도록 만들어준다.


ctrl + alt + b로 상속받고 있는 것들을 확인할 수 있다.
public class Util {
// -1, 0, 1
public static <T extends Number> int compare(T t1, T t2){
// 실수형
double value1 = t1.doubleValue(); // 매개변수로 입력받은 t1을 double 타입으로 변환해준 것. compare을 사용하기 위해
double value2 = t2.doubleValue();
return Double.compare(value1, value2);
}
}
public class BoundedTypeExample {
public static void main(String[] args) {
int result = Util.compare(1, 2);
System.out.println(result);
int result2 = Util.compare(4.5, 1.2);
System.out.println(result2);
int result3 = Util.compare(3, 3);
System.out.println(result3);
}
}
-1
1
0
문자열 비교시 에러 발생된다.

compare 메서드
제네럴에서는 T(타입)를 가장 많이 사용하고, 엘리먼트의 약자인 E도 사용한다.(주로 약어를 사용)
타입을 제한, 매개 변수 혹은 리턴 타입으로도 사용할 수 있다.
<?> : Unbounded Wildcards (제한 없음)<? extends 상위타입> : Upper Bounded Wildcards (상위 클래스 제한)<? super 하위타입> : Lower Bounded Wildcards (하위 클래스 제한) package chap10.wildcard;
public class Person {
private String name;
// 생성자는 리턴 타입이 없어도 된다.
public Person(String name){
this.name = name;
}
// 메서드는 리턴 타입이 있어야 한다.
public String getName() {
return name;
}
// toString 재정의, 해쉬값이 아닌 이름이 출력될 수 있도록
@Override
public String toString() { // Object 에서 물려 받음
// return super.toString(); // 부모의 toString 출력
return name; // 입력 받은 이름을 바로 리턴할 수 있게 return getName 으로 해도 됨
}
}
// 자식 클래스
package chap10.wildcard;
public class Worker extends Person{
public Worker(String name){
super(name);
}
}
package chap10.wildcard;
public class Student extends Person{
Student(String name){
super(name);
}
}
package chap10.wildcard;
public class HighStudent extends Student {
HighStudent(String name){
super(name);
}
}
package chap10.wildcard;
public class Course<T>{
private String name;
private T[] students; // Person[] students
// 생성자(리턴타입은 없으나, 접근 제한은 가능함)
Course(String name, int capacity){
this.name = name; // 입력 받은 이름으로 현재 필드 초기화
this.students = (T[]) new Object[capacity]; // 배열 초기화, 가상의 값이기 때문에 Object 로 배열의 객체를 생성, 배열의 길이 지정 Object 라는 타입이기 때문에 캐스팅 해주어야 한다.course의 타입을 지정하는 당시에 배열의 타입이 지정이 될 것임.
}
// name 이 private 이기 때문에 외부에서 호출할 수 있에 getter 메서드 생성
public String getName() {
return name;
}
// students 가 private 이기 때문에 외부에서 호출할 수 있에 getter 메서드 생성
public T[] getStudents() {
return students;
}
void add(T t){
for(int i = 0; i < students.length; i++){
if(students[i] == null){
students[i] = t;
break;
}
}
}
}
package chap10.wildcard;
import java.util.Arrays;
public class WildCardExample {
public static void registerPerson(Course<?> t){
t.getStudents(); // 목록 호출
Arrays.toString(t.getStudents()); // 배열 각각의 요소 출력
System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents())); // 각각의 객체의 주소값(해쉬값)이 출력됨, Object 메서드에 그렇게 선언되어 있음
}
public static void registerPerson2(Course<? extends Student> t){
System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents()));
}
public static void registerPerson3(Course<? super Worker> t){
System.out.println(t.getName() + ": " + Arrays.toString(t.getStudents()));
}
public static void main(String[] args) {
// Course<Person> personCourse = new Course<>(); // 디폴트 생성자 호출
Course<Person> personCourse = new Course<>("일반인 과정", 4); // 디폴트 생성자 호출
personCourse.add(new Person("일반인"));
personCourse.add(new Student("학생"));
personCourse.add(new Worker("직장인"));
personCourse.add(new HighStudent("고등학생"));
Course<Worker> workerCourse = new Course<>("직장인 과정",4);
workerCourse.add(new Worker("직장인2"));
Course<Student> studentCourse = new Course<>("학생 과정", 4);
studentCourse.add(new Student("학생3"));
studentCourse.add(new HighStudent("고등학생3"));
Course<HighStudent> highStudentCourse = new Course<>("고등학생 과정", 4);
highStudentCourse.add(new HighStudent("고등학생4"));
System.out.println("============");
registerPerson3(workerCourse);
registerPerson3(personCourse);
System.out.println("============");
registerPerson2(studentCourse);
registerPerson2(highStudentCourse);
System.out.println("============");
registerPerson(personCourse);
registerPerson(workerCourse);
registerPerson(studentCourse);
registerPerson(highStudentCourse);
}
}
package chap10.wildcard;
public class CourseTypeExample {
public static void main(String[] args) {
Person person = new Person("일반인");
Person person2 = new Student("학생");
Person person3 = new Worker("직장인");
Person person4 = new HighStudent("고등학생");
Course<Person> personCourse = new Course<>("일반인 과정", 4);
personCourse.add(person);
// 위 코드를 한줄로 표현하면 personCourse.add(new Person()); Ctrl + Alt + N
Course<Worker> workerCourse = new Course<>("직장인 과정", 5);
workerCourse.add(new Worker("직장인2")); // worker와 worker의 자식만 들어갈 수 있다.
Course<Student> studentCourse = new Course<>("학생 과정", 4);
studentCourse.add(new Student("학생3"));
studentCourse.add(new HighStudent("고등학생3"));
Course<HighStudent> highStudentCourse = new Course<>("고등학생 과정", 4);
highStudentCourse.add(new HighStudent("고등학생4"));
}
}
제네릭 타입도 상속받을 수 있도록 정의해야 한다. 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있다. 상속 관계에서는 부모 클래스 타입이 자식 클래스를 품을 수 있다.
package chap10.inherit;
// 부모 제네릭 클래스
public class Product<T, M>{
private T kind;
private M model;
// 생성자
Product(T kind, M model) {
this.kind = kind; // 타입 추론
this.model = model;
}
public T getKind() {
return kind;
}
public M getModel(){
return model;
}
}
package chap10.inherit;
public class ChildProduct<T, M, C> extends Product<T, M> {
private C company;
// 생성자
ChildProduct(T kind, M model, C company){
super(kind, model); // 상속 받은 것임으로 부모 호출
this.company = company;
}
public C getCompany() {
return company;
}
}
컴파일 되는 시점에 타입이 유연하게 바뀔 수 있다.
ChildProduct<String, String, String> → ChildProduct<Tv, String, String>
package chap10.inherit;
import java.util.ArrayList;
public class InheritGenericExample {
public static void main(String[] args) {
// 타입 = 클래스이기 때문에 Tv가 타입이 된다.
Tv tv = new Tv();
ChildProduct<Tv, String, String> childProduct = new ChildProduct(new Tv(), "galaxy book", "samsung"); // new Tv() 생성자를 호출해서 객체를 만들어줌, 첫번째 제네릭 타입은 Tv 라는 타입이 된다.
// ArrayList<Tv> tvList = new ArrayList<>();
String company = childProduct.getCompany();
System.out.println(childProduct.getCompany());
}
}
제네릭 인터페이스를 구현한 클래스도 제네릭 타입이 된다.
package chap10.implement;
// 데이터를 들고 있는 저장소라고 생각
public interface Storage<T> {
void add(T item, int index); // 값을 넣어줄 수도 있고
T get(int index); // 값을 get 해줄 수도 있다.
}
제네릭 인터페이스인 Storage 타입을 구현한 StorageImpl 클래스도 제네릭 타입이어야한다.
package chap10.implement;
// Stogage 의 구현체
// 구현체에서도 타입 파라미터를 가져다 써야 한다.
public class StorageImpl<T> implements Storage<T> {
private T[] array; // 필드 생성
// 생성자
public StorageImpl(int capacity){
array = (T[]) new Object[capacity]; // 배열 생성
}
@Override
public void add(T item, int index) {
array[index] = item;
}
@Override
public T get(int index) {
return array[index];
}
}
package chap10.implement;
import java.util.ArrayList;
public class ImplementExample {
public static void main(String[] args){
StorageImpl<String> storage = new StorageImpl<>(10);
storage.add("첫번째",0);
storage.add("두번째",1);
storage.add("세번째",2);
String result = storage.get(1);
System.out.println(result);
// ArrayList 사용
ArrayList<String> storageList = new ArrayList<>();
storageList.add(0, "문자열1");
storageList.add(1, "문자열2");
storageList.get(1);
}
}