안녕하세요? 오늘은 중요하지만, 한 번에 다루기에 애매했던 토픽들에 대해서 배워 보고자 합니다.
Generic은 C++ 에서의 Template와 같은 역할을 합니다. 타입 변수를 받아, 이에 맞는 지네릭 클래스를 만들 수 있습니다. 문법은 **C++**의 것과 상당히 유사 합니다. 클래스를 선언 할 때, 클래스명<타입 변수>
를 작성 하면 됩니다.
또한, 지네릭 메서드 또한 만들 수 있습니다. 메서드를 정의할 때, 리턴 타입까지 반환 하면 됩니다.
import java.util.ArrayList;
class MyNewArray<T> {
private ArrayList<T> array;
// 초기화 블록
{
array = new ArrayList<T>();
}
public T get(int i) {
return this.array.get(i);
}
public void add(T item) {
array.add(item);
}
public void print() {
for (int i = 0; i < this.array.size(); i++) {
System.out.println(this.array.get(i));
}
}
}
public class Main {
public static void main(String[] args) {
MyNewArray<Integer> array = new MyNewArray<>();
array.add(1);
array.add(2);
array.add(3);
array.print();
System.out.println("0번째 인덱스: " + array.get(0));
}
}
1
2
3
0번째 인덱스: 1
위와 같이 클래스명<타입변수>
형태로 클래스를 생성 했을 때, 절대 다른 타입변수가 적용된 제네릭 클래스의 객체를 참조할 수 없다는 점입니다. 예시를 들어 보겠습니다. 당연히 SomeClass<Fruit> sc = new SomeClass<Toy>();
같은 해당 코드는 작동 하지 않을 것 입니다. 아예 다른 클래스니까요, 그 다음으로, Fruit
이라는 상위 클래스가 존재하고, Apple
이라는 하위 클래스가 존재 한다면, Fruit
이 대입 된 지네릭 클래스에 Apple
은 대입 할 수 없습니다. 그렇게 해 놓은 이유는, 의도치 않은 결과가 나오지 않게 끔 하기 위해서 입니다. 클래스명<타입변수>
에서, 타입변수는 어느 클래스든 들어 갈 수 있기 때문이에요.
비슷한 이유로, 타입 변수 내에 있는, 가장 기본이 되는 Object
객체에 없는 메서드는 지네릭 클래스 안에서 실행 못하게 하는 것도 같은 이유에요.
SomeClass<Fruit> sc = new SomeClass<Fruit>(); // 가능
SomeClass<Fruit> sc = new SomeClass<Toy>(); // 불가능
SomeClass<Fruit> sc = new SomeClass<Apple>(); // 불가능
우리가 지네릭 클래스를 사용 하면서, 다형성을 이용 할 수 있게 하고 싶으면, 어떻게 하면 될까요? 지네릭을 제한 시키는 것이 도움이 될 수 있습니다. 한 번 보겠습니다. 만약 클래스명<타입변수 extends 부모클래스>
를 이용하여, 부모 클래스 자신 혹은, 부모 클래스의 자식 클래스만 사용 하도록 만들면 어떨까요? 그렇게 되면, 부모 클래스 내에 있는 메서드들은 지네릭 클래스를 구현 하여 사용할 수 있게 됩니다.
또한, 우리가 지네릭 클래스가 아닌 클래스에서 지네릭 타입에 따라서, 함수가 실행 되게 하기 위해선 어떻게 해야 할까요? 클래스명<타입변수>
처럼 되어 있는 것이 아니기 때문에, 타입변수를 사용 할 수 없을 텐데 말이죠. 그럴 때는 리턴 타입, 혹은 파라미터 타입에 와일드 카드를 사용하면 됩니다.
와일드 카드는 전체 클래스에 대해서 작동하게 하려면 다음과 같은 세 가지 방법이 있습니다.
<? extends T>
: T
와 그 자손들만 가능<? super T>
: T
와 그 조상들만 가능<?>
: 모든 타입이 가능.class Fruit {
public String myName() {return "Fruit";}
}
class Apple extends Fruit {
@Override
public String myName() {
return "Apple";
}
}
class Banana extends Fruit {
@Override
public String myName() {
return "Banana";
}
}
class MyClass<T extends Fruit> {
private T my_fruit;
public MyClass(T t) {
my_fruit = t;
}
public void print() {
System.out.println(this.my_fruit.myName()); // Fruit의 메서드를 사용 할 수 있다.
}
}
public class Main {
public static void callPrint(MyClass<? extends Fruit> mc) {
mc.print();
}
public static MyClass<?> returnMyClass(MyClass<?> mc) {
return mc;
}
public static void main(String[] args) {
MyClass<Fruit> f = new MyClass<Fruit>(new Fruit());
f.print();
callPrint(f);
MyClass<Apple> a = new MyClass<Apple>(new Apple());
a.print();
callPrint(a);
MyClass<Banana> b = new MyClass<Banana>(new Banana());
b.print();
callPrint(b);
}
}
Fruit
Fruit
Apple
Apple
Banana
Banana
**C++**에서 Enum
을 이용한 열거형이 있듯이, Java에도 열거형이 존재 합니다. Java의 열거형은, **C++**의 것보다 더 향상된 열거형 입니다. 열거형의 값 뿐만 아니라, 타입까지 체킹 하기 때문에, 보다 더 논리적인 오류를 줄일 수 있습니다. 또한, 열거형에 멤버변수 혹은 메서드를 추가 할 수 있습니다. 그리하여, 더 객체지향 적인 Enum Class를 만들 수 있습니다.
switch
문을 사용 하려고 할 때는, 열거형 타입을 굳이 입력 하지 않아도 됩니다.
String name()
: 열거형 객체의 이름을 반환 합니다.int ordinal()
: 열거형 객체의 순서를 반환 합니다.enum Alphabet {A, B, C, D, E, F}
enum Qwerty {
Q(1), W(3), E(5), R(7), T(9), Y(11);
// 열거형에 멤버 변수 및 메서드를 적용.
private final int value;
Qwerty(int value) {
this.value = value;
}
public int getValue() {return this.value;}
}
public class Main {
public static void main(String[] args) {
// System.out.println("Alphabet.A == Qwerty.Q: " + (Alphabet.A == Qwerty.Q)); => 불가능
System.out.println("Alphabet.A == Qwerty.Q: " + (Alphabet.A == Alphabet.B)); // 가능
Alphabet alphabet = Alphabet.A;
switch (alphabet) {
case A:
System.out.println("A 입니다!");
break;
default:
System.out.println("A가 아닙니다!");
break;
}
System.out.println(Alphabet.C.name());
System.out.println(Qwerty.R.ordinal());
System.out.println(Qwerty.Y.getValue());
}
}
Alphabet.A == Qwerty.Q: false
A 입니다!
C
3
11
**람다식(Lambda Expression)**은 메서드를 하나의 **식(Expression)**으로 나타 낸 것 입니다. 우리는 익명 함수를 이용하여, 파라미터를 함수로 받는 함수에 익명 함수를 파라미터로 삽입하여 다른 함수를 따로 구현 할 필요 없이, 이를 구현 할 수 있습니다.
import java.util.*;
public class Main {
public static void main(String[] args) {
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> 2 * (i + 1));
System.out.println(Arrays.toString(arr));
}
}
[2, 4, 6, 8, 10]
람다식은 다음과 같은 형식을 따릅니다.
(매개변수) -> { 문장들 }
만약 a, b
중 더 큰 값을 반환하는 max(int a, int b)
함수를 구현 한다고 하면, 어떻게 하면 될까요? 예시는 다음과 같습니다.
(int a, int b) -> {return a > b ? a : b;}
(a, b) -> {return a > b ? a : b;}
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b // 타입 추론이 가능 한 경우
여기서, 매개변수가 하나만 존재 한다면, a -> a * a
로 표현 가능 합니다.
만약, 우리가 람다식을 파라미터로 넣을 수 있다면, 그 람다식은, 특정 클래스에 포함 되어야 한다는 의미를 가집니다. 그렇다면, 람다식은 어떤 클래스 혹은 인터페이스에 들어가 있는 걸까요?
사실, 그것은 우리가 정해주면 되는 것 입니다. 우리는 다음과 같은 조건을 만족하는 인터페이스를 만들어서 제공 하면 됩니다.
@FunctionalInterface
를 넣어 줍니다.public abstract 반환타입 함수명(파라미터)
로 말입니다.다음은 구현 예시 입니다.
@FunctionalInterface
interface MyFunction {
public abstract int func(int x);
}
public class Main {
public static void printFunc(int x, MyFunction f) {
System.out.println(f.func(x));
}
public static void main(String[] args) {
printFunc(2, x -> x * x);
}
}
4
우리는 x -> x * x
람다식을 구현 후, 파라미터로 넣음으로써 이를 구현 할 수 있었습니다. 사실, 위의 코드에서 x -> x * x
는 다음과 동일 합니다. 우리는 @FunctionalInterface
어노테이션을 통해, 어느 함수로 우리가 만든 익명 함수로 매핑해야 하는 지 알고 있습니다. 당연히, 위의 규칙 대로라면, 유일하게 구현 해야 하는 추상 메서드로 매핑이 될 것 입니다.
new Object() {
public int func(int x) {
return x * x;
}
}
우리는 이런식으로, @FunctionalInterface
를 구현 하여, 람다식을 함수의 파라미터로 이용 할 수 있도록 할 수 있었습니다.
Java에서는 기본적으로 Function Interface를 제공 합니다. 바로 java.util.function
에서 말이죠. 일단, 많은 것을 다루지 않고, 기초적인 것만 다루고 넘어 가도록 하겠습니다.
매개변수도 없고, 반환 값도 없는 인터페이스입니다. Thread 생성 시 자주 사용 합니다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> { // 해당 위치에서 사용 됨
try {
System.out.println("쓰레드 시작!");
Thread.sleep(1000);
System.out.println("쓰레드 끝!");
} catch (InterruptedException e) {}
});
thread.start();
}
}
쓰레드 시작!
쓰레드 끝!
매개변수는 없고, 반환 값 T
만 있는 인터페이스 입니다. T get()
을 구현하는 인터페이스 입니다.
import java.util.function.*;
public class Main {
public static void printFunc(Supplier<?> t) { // 와일드 카드 사용
System.out.println(t.get()); // T get()
}
public static void main(String[] args) {
printFunc(() -> 3.14); // 알아서 변환 됨
}
}
3.14
매개변수로 T
를 받고, 반환 값은 없는 인터페이스 입니다. void accept(T t)
를 구현하는 인터페이스 입니다.
import java.util.function.*;
public class Main {
public static void useFunc(int x, Consumer<Integer> func) {
func.accept(x);
}
public static void main(String[] args) {
useFunc(5, (x) -> {System.out.println(x + "가 입력 되었습니다.");});
}
}
5가 입력 되었습니다.
매개변수로 T
를 받고, R
을 반환 하는 인터페이스 입니다. R apply(T t)
를 구현하는 인터페이스 입니다.
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Function<Integer, String> consumer = (x) -> x + ""; // 생성 과정에서 바로 람다식 대입 가능
System.out.println(consumer.apply(1234));
}
}
1234
매개변수로 T
를 받고, boolean
을 반환 하는 인터페이스 입니다. boolean test(T t)
를 구현 합니다.
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Predicate<int[]> predicate = (int[] array) -> array.length == 0;
int[] array = {1, 2, 3};
System.out.println(predicate.test(array));
}
}
false
기타 인터페이스로 두 개의 파라미터를 받으며 똑같은 기능을 하는 BiConsumer<T, U>
, BiPredicate<T, U>
, BiFunction<T, U, R>
가 있고, 매개변수 T
한 개를 받아 T
로 반환하는 UnaryOperator<T>
, 매개변수 T
두 개를 받아 하나의 T
로 반환하는 BinaryOperator<T>
가 있습니다. 그 이외에서 많은 인터페이스가 존재 합니다.
import java.util.function.*;
public class Main {
public static void main(String[] args) {
BiConsumer<Integer, Float> biConsumer = (Integer i, Float f) -> {System.out.println(i + f);};
BiPredicate<Integer, Float> biPredicate = (Integer i, Float f) -> i > f;
BiFunction<Integer, Float, Float> biFunction = (Integer i, Float f) -> (float)i + f;
UnaryOperator<Integer> unaryOperator = (Integer a) -> a * a;
BinaryOperator<Integer> binaryOperator = (Integer a, Integer b) -> 2 * a + b;
}
}
다음 시간에는 배열, 컬렉션처럼 많은 데이터를 처리 할 수 있는 Stream에 대해서 배워 보도록 하겠습니다.