diff --git a/java/effective-java/docs/05-generics/item31.md b/java/effective-java/docs/05-generics/item31.md new file mode 100644 index 00000000..b298ab03 --- /dev/null +++ b/java/effective-java/docs/05-generics/item31.md @@ -0,0 +1,238 @@ +# item31 한정적 와일드카드를 사용해 API 유연성을 높이라 + + + +### 개요 + +- 매개변수화 타입은 불공변이다. +- List 은 List 의 일을 모두 수행 못하니 당연하다 볼수 있따. (item10 리스코프 원칙 위배) +- 불공변 보다 유연한게 필요하다. + + + +아래 예는 매개변수화 타입이 불공변이기 때문에 컴파일에러 발생한다. + +~~~java +Stack numberStack = new Stack<>(); +Iterable integers = Arrays.asList(3, 1, 4, 1, 5, 9); +numberStack.pushAll(integers); +~~~ + +~~~java +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +~~~ + +~~~sh +# 컴파일에러 발생 +java: incompatible types: java.lang.Iterable cannot be converted to java.lang.Iterable +~~~ + + + +- 한정적 와일드카드 타입으로 해결할 수 있다. (유연하게 !) + +~~~java +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +~~~ + + + +## 1. PECS + + + +### 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라 + +- 다만 생산자, 소비자 두 역할 모두 한다면 와일드카드를 사용하지 않는다 .지정을 해야 한다. + + + +### PECS: producer-extends, consumer-super + +- PECS 공식은 와일드카드 타입을 사용하는 기본 원칙이다. +- 생산자 : + - T 타입을 리소스에 저장하는 형태. + - T 타입으로 선언된 자료구조에 저장되야 하니, ? 는 T를 상속하는 형태여야 한다. +- 소비자: + - T 타입을 리소스에서 꺼내는 형태. + - T 를 꺼내서 외부 리소스에 저장해야 하니, T 이거냐 T가 상속(확장) 한 타입이어야 한다. + + + +### 반환타입에서 와일드카드타입? + +- 반환타입은 Set 이다. + - 반환타입에는 한정적 와일드카드 타입을 사용하면 안된다. + - 유연성 x, 클라이언트에 와일드카드 타입을 써야 함. +- **만약 클래스 사용자가 와일드카드 타입을 신경써야 한다면, API에 문제가 있을 가능성이 있다.** + +~~~java +public static Set union(Set s1, Set s2) { +~~~ + +- 위 메서 드 사용 시 리턴타입에 대해서는 자바가 추론하여 자동으로 형변환 해준다. (java 8부터 가능) + - 만약 자바7 이하라면, 명시적 타입 변환을 해주어야 한다. + + + + + +## 2. Comparator와 Comparable은 소비자 + +- 아래 예제는 생산자와 소비자가 중첩되게 사용 되었다. +- E는 데이터를 쌓으니 생산자가 되고, +- Comparable는 데이터를 꺼내서 비교하니 소비자가 된다. + +~~~java +public static > E max(List list) { +~~~ + +- 만약 Comparable 로 정의한다면, 보통은 문제 없지만, 리턴타입이 와일드 카드일 경우 컴파일에러가 발생한다. + - E의 구체적으로 어떤 상위타입인지 추론할 수 없기 때문. + - 실제로 부모타입만 Comparable를 구현했을 수 있기 때문에 소비자로서 작성해야 함. + +~~~java +List list = new ArrayList<>(); +list.add(new IntegerBox(10, "effective")); +list.add(new IntegerBox(2, "java")); + +System.out.println(max(list)); +Box max = max(list); // 컴파일에러 +~~~ + +~~~java +// 컴파일에러 +java: incompatible types: inference variable E has incompatible equality constraints +me.staek.chapter05.item31.pecs.Box,me.staek.chapter05.item31.pecs.Box +~~~ + + + + + +## 3. 와일드카드 활용 + +- 메서드 인자의 와일드카드 vs 매개변수화 타입 비교 + +~~~java +public static void swap(List list, int i, int j) +public static void swap(List list, int i, int j) +~~~ + +### 기본규칙 : 메서드 선언 타입매개변수가 한번만 나오면 와일드카드로 대체하라. + +- 비한정적 타입 매개변수라면 비한정적 와일드카드로 바꾸고 +- 한정적 타입 매개변수라면 한정적 와일드카드로 바꾼다. + + + +### 문제점 + +- 컴파일이 안된다. + - 매개변수로 와일드카드가 입력된 후, 다시 매개변수로 set에 전달한다면 + - **set은 해당 매개변수가 와일드카드로 입력되길 예상하기 때문에 컴파일에러가 발생한다.** + +~~~java +public static void swap(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +~~~ + +### 해결방법 + +- 실제타입으로 변경해주는 메서드를 구현하여 해결한다. + +~~~java +public static void swap(List list, int i, int j) { + swapHelper(list, i, j); +} + +// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 +private static void swapHelper(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +~~~ + +### 나라면? + +- 이게 뭐가 복잡한가. 그냥 이거 쓰는게 좋다. +- 와일드카드는 PECS 인경우 한정적 와일드타입으로 변경할 때만 사용하고 그 외 단독으로는 사용하지 않는게 더 신경 쓸 일 없고 좋아보임. + +```java +public static void swap(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +``` + + + + + +## 4. 타입 추론 (Type Inference) + +### 타입추론 + +~~~java +/** + * 제네릭 메서드 인수에 대한 명시적타입인수 + */ +ArrayList> listOfIntegerBoxes = new ArrayList<>(); +BoxExample.addBox(10, listOfIntegerBoxes); // 입력되는 10을 보고 리턴 타입을 추론하는 것임. +BoxExample.addBox(20, listOfIntegerBoxes); +~~~ + + + +~~~java +/** + * Target Type + * 메서드 리턴타입이 제네릭인데, 명시적 형변환 없이 타입추론이 가능. + */ +List stringlist = Collections.emptyList(); +List integerlist = Collections.emptyList(); +~~~ + + + +~~~java +/** + * Target Type + * 메서드 인자 타입추론 + */ +BoxExample.processStringList(Collections.emptyList()); +BoxExample.processStringList(Collections.emptyList()); +~~~ + + + +### 타입추론의 한계 + +~~~java +/** + * TODO 타입추론 한계 + * - comparingInt함수 인자에 명시적형변환을 해야한다. 이후 체이닝 메서드에서는 안해도 된다. + * - comparingInt함수 인자는 Consumer 여서 정확히 어떤 상위타입인지 추론이 불가능하기에 지정해주어야 하는 듯하다. + */ +private static final Comparator COMPARATOR = + comparingInt((PhoneNumberComparatorTest pn) -> pn.areaCode) + .thenComparingInt(pn -> pn.getPrefix()) + .thenComparingInt(pn -> pn.lineNum); +~~~ + + + + + +## 5. 정리 + +조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다. +그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다. +PECS공식을 기억하자. +즉, 생사낮(producer)는 extends를 소비자(consumer)는 super를 사용한다. +Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자. \ No newline at end of file diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/Swap.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/Swap.java new file mode 100644 index 00000000..013e997d --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/Swap.java @@ -0,0 +1,42 @@ +package me.staek.chapter05.item31; + + +import java.util.Arrays; +import java.util.List; + +/** + * 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 + */ +public class Swap { + + /** + * 굳이 비한정적타입(?) 을 사용하지말고 E를 정의해서 사용해보자. + */ + public static void swap(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); + } + + /** + * 비한정적타입(?)으로 인자를 받고 사용하는 건 상관 없으나, + * 이를 다시 할당할 경우, 받는쪽에서는 똑같이 비한정적타입을 예상하기에 컴파일에러가 발생한다. + * + * 굳이 비한정적타입을 사용한다면, 와일드카드를 실제타입으로 바꿔주는 메서드(swapHelper)를 정의해서 사용하면 되지만, + * 오히려 복잡하니 그냥 E 를 정의해서 사용하는것도 고려해 보자. + */ +// public static void swap(List list, int i, int j) { +//// list.set(i, list.set(j, list.get(i))); +// swapHelper(list, i, j); +// } + + // 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드 + private static void swapHelper(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); + } + + public static void main(String[] args) { + // 첫 번째와 마지막 인수를 스왑한 후 결과 리스트를 출력한다. + List argList = Arrays.asList(args); + swap(argList, 0, argList.size() - 1); + System.out.println(argList); + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Box.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Box.java new file mode 100644 index 00000000..2e169109 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Box.java @@ -0,0 +1,27 @@ +package me.staek.chapter05.item31.pecs; + +public class Box> implements Comparable> { + + protected T value; + + public Box(T value) { + this.value = value; + } + + public void change(T value) { + this.value = value; + } + + @SuppressWarnings("unchecked") + @Override + public int compareTo(Box anotherBox) { + return this.value.compareTo((T)anotherBox.value); + } + + @Override + public String toString() { + return "Box{" + + "value=" + value + + '}'; + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Chooser.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Chooser.java new file mode 100644 index 00000000..bd02dff1 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Chooser.java @@ -0,0 +1,34 @@ +package me.staek.chapter05.item31.pecs; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * T 생산자 매개변수에 와일드카드 타입 적용 + */ +public class Chooser { + private final List choiceList; + private final Random rnd = new Random(); + + /** + * 생산자이기 때문에 형태로 작성한다. + */ + public Chooser(Collection choices) { + choiceList = new ArrayList<>(choices); + } + + public T choose() { + return choiceList.get(rnd.nextInt(choiceList.size())); + } + + public static void main(String[] args) { + List intList = List.of(1, 2, 3, 4, 5, 6); + Chooser chooser = new Chooser<>(intList); + for (int i = 0; i < 10; i++) { + Number choice = chooser.choose(); + System.out.println(choice); + } + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/EmptyStackException.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/EmptyStackException.java new file mode 100644 index 00000000..80dc8b31 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/EmptyStackException.java @@ -0,0 +1,4 @@ +package me.staek.chapter05.item31.pecs; + +public class EmptyStackException extends RuntimeException { +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/IntegerBox.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/IntegerBox.java new file mode 100644 index 00000000..37483a9e --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/IntegerBox.java @@ -0,0 +1,19 @@ +package me.staek.chapter05.item31.pecs; + +public class IntegerBox extends Box { + + private final String message; + + public IntegerBox(int value, String message) { + super(value); + this.message = message; + } + + @Override + public String toString() { + return "IntegerBox{" + + "message='" + message + '\'' + + ", value=" + value + + '}'; + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/RecursiveTypeBound.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/RecursiveTypeBound.java new file mode 100644 index 00000000..5cd7ad54 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/RecursiveTypeBound.java @@ -0,0 +1,39 @@ +package me.staek.chapter05.item31.pecs; + + +import java.util.ArrayList; +import java.util.List; + + +/** + * 재귀적 타입 한정 : 생성자,소비자 두번 중첩. (> ) + * - Comparable는 존재하는 정보를 꺼내서 사용하기에 소비자에 속한다. 따라서 를 작성한다. + * - 아래 예제에서 E 는 IntegerBox 타입인데, Box가 상속한 부모만 Comparable를 구현한 경우에 에러가 발생할 수 있다. + * => Box max = max(list); 리턴타입이 Integer라는 타입을 정해주면 에러가 나지 않지만 + * => Box max = max(list); 정해지지 않은 타입이라면, 컴파일에러가 발생한다. + * - + */ +public class RecursiveTypeBound { + public static > E max(List list) { + if (list.isEmpty()) + throw new IllegalArgumentException("빈 리스트"); + + E result = null; + for (E e : list) + if (result == null || e.compareTo(result) > 0) + result = e; + + return result; + } + + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(new IntegerBox(10, "effective")); + list.add(new IntegerBox(2, "java")); + + System.out.println(max(list)); +// Box max = max(list); // + Box max = max(list); + System.out.println(max.toString()); + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Stack.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Stack.java new file mode 100644 index 00000000..203d1b05 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Stack.java @@ -0,0 +1,106 @@ +package me.staek.chapter05.item31.pecs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EmptyStackException; + +/** + * 와일드카드 타입을 이용한 생산자,소비자 (PECS) 사용 예제 (main문에서 설명) + */ +public class Stack { + private E[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + @SuppressWarnings("unchecked") + public Stack() { + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(E e) { + ensureCapacity(); + elements[size++] = e; + } + + public E pop() { + if (size==0) + throw new EmptyStackException(); + E result = elements[--size]; + elements[size] = null; + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } + + /** + * 와일드카드 타입을 사용하지 않은 pushAll 메서드 + */ +// public void pushAll(Iterable src) { +// for (E e : src) +// push(e); +// } + + /** + * E 생산자(producer) 매개변수에 와일드카드 타입 적용 + */ + public void pushAll(Iterable src) { + for (E e : src) + push(e); + } + + + + /** + * 와일드카드 타입을 사용하지 않은 popAll 메서드 + */ +// public void popAll(Collection dst) { +// while (!isEmpty()) +// dst.add(pop()); +// } + + /** + * E 소비자(consumer) 매개변수에 와일드카드 타입 적용 + */ + public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); + } + + + public static void main(String[] args) { + /** + * 생산자 예제 + * 불공변이므로 다음은 성립하지 않는다. Iterable <<-- Iterable 따라서 컴파일 에러가 발생한다. + * 하지만 논리적으로 Integer 타입은 Number 타입으로 추상화하여 사용할 수 있다. + * 이를 만족시키기 위해 pushAll(Iterable src) ==> pushAll(Iterable src) 이렇게 구성한다. + * ==> 생산자는 어떤 인자라 입력되었을 때 정보를 쌓아간다는 의미로서, E타입을 상속한 무엇이든 상관 없음을 내포한다. + */ + Stack numberStack = new Stack<>(); + Iterable integers = Arrays.asList(3, 1, 4, 1, 5, 9); + numberStack.pushAll(integers); + + + Iterable doubles = Arrays.asList(3.1, 1.0, 4.0, 1.0, 5.0, 9.0); + numberStack.pushAll(doubles); + + /** + * 소비자 예제 + * popAll메서드 인자로 전달 될 때 Collection <<-- Collection 는 성립되지 않는다 + * 하지만 논리적으로 Number 타입을 꺼내서 Object 타입으로 전달할 의도이기에 문제가 없다. + * 이를 만족시키기 위해 popAll(Collection dst) ==>> popAll(Collection dst) 이렇게 구성한다. + * ==> 소비자는 이미 존재하는 리소스를 꺼내서 입력된 인자에 넣어주는 경우를 의미하므로, E타입이 상속한 무엇이든 상관 없음을 내포한다. + */ + Collection objects = new ArrayList<>(); + numberStack.popAll(objects); + + System.out.println(objects); + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Union.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Union.java new file mode 100644 index 00000000..15705381 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/pecs/Union.java @@ -0,0 +1,37 @@ +package me.staek.chapter05.item31.pecs; + +import java.util.HashSet; +import java.util.Set; + +/** + * T 생산자 매개변수에 와일드카드 타입 적용 + */ +public class Union { + + /** + * 생산자이므로 를 작성한다. + */ + public static Set union(Set s1, + Set s2) { + Set result = new HashSet<>(s1); + result.addAll(s2); + return result; + } + + public static void main(String[] args) { + Set integers = new HashSet<>(); + integers.add(1); + integers.add(3); + integers.add(5); + + Set doubles = new HashSet<>(); + doubles.add(2.0); + doubles.add(4.0); + doubles.add(6.0); + + Set numbers = union(integers, doubles); +// Set numbers = Union.union(integers, doubles); // 타입추론 until java7 + + System.out.println(numbers); + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/Box.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/Box.java new file mode 100644 index 00000000..f2f3f4a0 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/Box.java @@ -0,0 +1,14 @@ +package me.staek.chapter05.item31.typeinference; + +public class Box { + + private T t; + + public T get() { + return t; + } + + public void set(E e) { + this.t = e; + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/BoxExample.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/BoxExample.java new file mode 100644 index 00000000..c21134c0 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/BoxExample.java @@ -0,0 +1,61 @@ +package me.staek.chapter05.item31.typeinference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 여러가지 추론타입에대한 예제 + */ +public class BoxExample { + + private static void addBox(U u, List> boxes) { + Box box = new Box<>(); + box.set(u); + boxes.add(box); + } + + private static void outputBoxes(List> boxes) { + int counter = 0; + for (Box box: boxes) { + U boxContents = box.get(); + System.out.println("Box #" + counter + " contains [" + + boxContents.toString() + "]"); + counter++; + } + } + + private static void processStringList(List stringList) { + + } + + + public static void main(String[] args) { + + /** + * 제네릭 메서드 인수에 대한 명시적타입인수 + */ + ArrayList> listOfIntegerBoxes = new ArrayList<>(); + BoxExample.addBox(10, listOfIntegerBoxes); // 입력되는 10을 보고 리턴 타입을 추론하는 것임. + BoxExample.addBox(20, listOfIntegerBoxes); + BoxExample.addBox(30, listOfIntegerBoxes); + BoxExample.outputBoxes(listOfIntegerBoxes); + + + + /** + * Target Type + * 메서드 리턴타입이 제네릭인데, 명시적 형변환 없이 타입추론이 가능. + */ + List stringlist = Collections.emptyList(); + List integerlist = Collections.emptyList(); + + + /** + * Target Type + * 메서드 인자 타입추론 + */ + BoxExample.processStringList(Collections.emptyList()); + BoxExample.processStringList(Collections.emptyList()); + } +} diff --git a/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/PhoneNumberComparatorTest.java b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/PhoneNumberComparatorTest.java new file mode 100644 index 00000000..7d95b965 --- /dev/null +++ b/java/effective-java/effective-java/src/main/java/me/staek/chapter05/item31/typeinference/PhoneNumberComparatorTest.java @@ -0,0 +1,95 @@ +package me.staek.chapter05.item31.typeinference; + +import java.util.Comparator; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ThreadLocalRandom; + +import static java.util.Comparator.comparingInt; + +/** + * 타입추론 한계 (item14 예제) + * private static final Comparator COMPARATOR 정의 참고 + * + */ +public final class PhoneNumberComparatorTest implements Cloneable, Comparable { + private final short areaCode, prefix, lineNum; + + public short getAreaCode() { + return areaCode; + } + + public short getPrefix() { + return prefix; + } + + public short getLineNum() { + return lineNum; + } + + public PhoneNumberComparatorTest(int areaCode, int prefix, int lineNum) { + this.areaCode = rangeCheck(areaCode, 999, "지역코드"); + this.prefix = rangeCheck(prefix, 999, "프리픽스"); + this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호"); + } + + private static short rangeCheck(int val, int max, String arg) { + if (val < 0 || val > max) + throw new IllegalArgumentException(arg + ": " + val); + return (short) val; + } + + @Override public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof PhoneNumberComparatorTest)) + return false; + PhoneNumberComparatorTest pn = (PhoneNumberComparatorTest)o; + return pn.lineNum == lineNum && pn.prefix == prefix + && pn.areaCode == areaCode; + } + + @Override public int hashCode() { + int result = Short.hashCode(areaCode); + result = 31 * result + Short.hashCode(prefix); + result = 31 * result + Short.hashCode(lineNum); + return result; + } + + + @Override public String toString() { + return String.format("%03d-%03d-%04d", + areaCode, prefix, lineNum); + } + + /** + * TODO 타입추론 한계 + * - comparingInt함수 인자에 명시적형변환을 해야한다. 이후 체이닝 메서드에서는 안해도 된다. + * - comparingInt함수 인자는 Consumer 여서 정확히 어떤 상위타입인지 추론이 불가능하기에 지정해주어야 하는 듯하다. + */ + private static final Comparator COMPARATOR = + comparingInt((PhoneNumberComparatorTest pn) -> pn.areaCode) + .thenComparingInt(pn -> pn.getPrefix()) + .thenComparingInt(pn -> pn.lineNum); + + @Override + public int compareTo(PhoneNumberComparatorTest pn) { + return COMPARATOR.compare(this, pn); + } + + private static PhoneNumberComparatorTest randomPhoneNumber() { + Random rnd = ThreadLocalRandom.current(); + return new PhoneNumberComparatorTest((short) rnd.nextInt(1000), + (short) rnd.nextInt(1000), + (short) rnd.nextInt(10000)); + } + + public static void main(String[] args) { + Set s = new TreeSet<>(); + for (int i = 0; i < 10; i++) + s.add(randomPhoneNumber()); + System.out.println(s); + } + +}