JavaRush /Java Blog /Random-KO /고양이용 제네릭
Viacheslav
레벨 3

고양이용 제네릭

Random-KO 그룹에 게시되었습니다
고양이용 제네릭 - 1

소개

오늘은 우리가 Java에 대해 알고 있는 것을 기억하기에 좋은 날입니다. 가장 중요한 문서에 따르면, 즉 Java 언어 사양(JLS - Java Language Specifiaction), Java는 " 4장. 유형, 값 및 변수 " 장에 설명된 대로 강력한 유형의 언어입니다 . 이것은 무엇을 의미 하는가? 기본 메소드가 있다고 가정해 보겠습니다.
public static void main(String[] args) {
String text = "Hello world!";
System.out.println(text);
}
강력한 유형 지정을 통해 이 코드를 컴파일할 때 컴파일러는 텍스트 변수의 유형을 문자열로 지정한 경우 이를 다른 유형의 변수(예: 정수)로 사용하려고 하지 않는지 확인합니다. . 예를 들어 텍스트 대신 값 2L(예: 문자열 대신 긴 값)을 저장하려고 하면 컴파일 타임에 오류가 발생합니다.

Main.java:3: error: incompatible types: long cannot be converted to String
String text = 2L;
저것들. 강력한 유형 지정을 사용하면 해당 작업이 해당 개체에 대해 적합한 경우에만 개체에 대한 작업이 수행되도록 할 수 있습니다. 이를 유형 안전성이라고도 합니다. JLS에 명시된 대로 Java에는 기본 유형과 참조 유형이라는 두 가지 유형 범주가 있습니다. 리뷰 기사에서 기본 유형에 대해 기억하실 수 있습니다: " Java의 기본 유형: 그다지 원시적이지 않습니다 ." 참조 유형은 클래스, 인터페이스 또는 배열로 표시될 수 있습니다. 그리고 오늘 우리는 참조 유형에 관심을 가질 것입니다. 배열부터 시작해 보겠습니다.
class Main {
  public static void main(String[] args) {
    String[] text = new String[5];
    text[0] = "Hello";
  }
}
이 코드는 오류 없이 실행됩니다. 우리가 알고 있듯이(예를 들어 " Oracle Java Tutorial: Arrays "에서) 배열은 한 가지 유형의 데이터만 저장하는 컨테이너입니다. 이 경우에는 라인만 가능합니다. String 대신 배열에 long을 추가해 보겠습니다.
text[1] = 4L;
이 코드를 실행하면(예: Repl.it Online Java Compiler 에서 ) 오류가 발생합니다.
error: incompatible types: long cannot be converted to String
배열과 언어의 유형 안전성으로 인해 유형에 맞지 않는 것을 배열에 저장할 수 없었습니다. 이는 유형 안전성의 표현입니다. 우리는 "오류를 수정하세요. 하지만 그때까지는 코드를 컴파일하지 않을 것입니다."라는 말을 들었습니다. 그리고 이것에 대한 가장 중요한 점은 이것이 프로그램이 시작될 때가 아니라 컴파일할 때 일어난다는 것입니다. 즉, 오류는 "언젠가"가 아닌 즉시 확인됩니다. 배열에 대해 기억했으니 Java Collections Framework 에 대해서도 기억해 봅시다 . 우리는 거기에 다른 구조를 가지고 있었습니다. 예를 들어 목록입니다. 예제를 다시 작성해 보겠습니다.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List text = new ArrayList(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(0);
  }
}
컴파일할 때 test변수 초기화 라인에서 오류가 발생합니다.
incompatible types: Object cannot be converted to String
우리의 경우 List는 모든 객체(즉, Object 유형의 객체)를 저장할 수 있습니다. 따라서 컴파일러는 그러한 책임을 질 수 없다고 말합니다. 따라서 목록에서 가져올 유형을 명시적으로 지정해야 합니다.
String test = (String) text.get(0);
이러한 표시를 유형 변환 또는 유형 캐스팅이라고 합니다. 이제 인덱스 1에 요소를 가져오려고 시도하기 전까지는 모든 것이 잘 작동할 것입니다. 왜냐하면 Long 유형입니다. 그러면 상당한 오류가 발생하지만 이미 프로그램이 실행되는 동안(런타임에서):

type conversion, typecasting
Exception in thread "main" java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
보시다시피 여기에는 몇 가지 중요한 단점이 있습니다. 첫째, 목록에서 얻은 값을 String 클래스로 "캐스트"해야 합니다. 동의하세요. 이건 추악합니다. 둘째, 오류가 발생하면 프로그램이 실행될 때만 볼 수 있습니다. 코드가 더 복잡하다면 이러한 오류를 즉시 감지하지 못할 수도 있습니다. 그리고 개발자들은 이러한 상황에서 작업을 더 쉽게 만들고 코드를 더 명확하게 만드는 방법에 대해 생각하기 시작했습니다. 그리고 그들은 태어났습니다 - Generics.
고양이용 제네릭 - 2

제네릭

그래서, 제네릭. 그것은 무엇입니까? 제네릭은 사용된 유형을 설명하는 특별한 방법으로, 코드 컴파일러가 유형 안전성을 보장하기 위해 작업에 사용할 수 있습니다. 다음과 같이 보입니다.
고양이용 제네릭 - 3
다음은 간단한 예와 설명입니다.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> text = new ArrayList<String>(5);
    text.add("Hello");
    text.add(4L);
    String test = text.get(1);
  }
}
이 예에서는 String 유형의 객체에만 작동하는 , List뿐만 아니라 가 있다고 말합니다 . List그리고 다른 사람은 없습니다. 괄호 안에 표시된 내용을 저장할 수 있습니다. 이러한 "괄호"를 "꺾쇠 괄호"라고 합니다. 꺾쇠 괄호. 컴파일러는 문자열 목록(목록 이름은 text)으로 작업할 때 실수가 있는지 친절하게 확인합니다. 컴파일러는 우리가 Long을 String 목록에 넣으려고 뻔뻔하게 노력하고 있음을 알게 될 것입니다. 그리고 컴파일 시간에 오류가 발생합니다.
error: no suitable method found for add(long)
String이 CharSequence의 자손이라는 것을 기억했을 것입니다. 그리고 다음과 같은 일을 하기로 결정합니다:
public static void main(String[] args) {
	ArrayList<CharSequence> text = new ArrayList<String>(5);
	text.add("Hello");
	String test = text.get(0);
}
하지만 이는 불가능하며 다음과 같은 오류가 발생합니다. error: incompatible types: ArrayList<String> cannot be converted to ArrayList<CharSequence> It Should wonder, 왜냐하면. 줄 CharSequence sec = "test";에 오류가 없습니다. 그것을 알아 봅시다. 그들은 이 행동에 대해 이렇게 말합니다. "제네릭은 변하지 않습니다." "불변"이란 무엇입니까? 나는 Wikipedia의 “ 공분산 및 반공분산 ” 기사에서 이에 대해 다음과 같이 설명하는 방식을 좋아합니다 .
고양이용 제네릭 - 4
따라서 불변성은 파생 유형 간에 상속이 없음을 의미합니다. Cat이 Animals의 하위 유형인 경우 Set<Cats>는 Set<Animals>의 하위 유형이 아니며 Set<Animals>는 Set<Cats>의 하위 유형이 아닙니다. 그런데 Java SE 7부터 소위 " 다이아몬드 오퍼레이터 "가 등장했다고 말할 가치가 있습니다. 두 개의 꺾쇠 괄호 <>는 다이아몬드와 같기 때문입니다. 이를 통해 다음과 같은 제네릭을 사용할 수 있습니다.
public static void main(String[] args) {
  List<String> lines = new ArrayList<>();
  lines.add("Hello world!");
  System.out.println(lines);
}
이 코드를 기반으로 컴파일러는 왼쪽에 ListString 유형의 개체가 포함될 것이라고 표시한 경우 오른쪽에는 lines새 ArrayList를 변수에 저장하고 개체도 저장한다는 의미임을 이해합니다. 왼쪽에 지정된 유형입니다. 따라서 왼쪽의 컴파일러는 오른쪽의 유형을 이해하거나 추론합니다. 이것이 바로 이 동작을 유형 추론 또는 영어로 "유형 추론"이라고 부르는 이유입니다. 주목할만한 또 다른 흥미로운 점은 RAW 유형 또는 "원시 유형"입니다. 왜냐하면 제네릭이 항상 존재하는 것은 아니며 Java는 가능할 때마다 이전 버전과의 호환성을 유지하려고 시도합니다. 그러면 제네릭은 제네릭이 지정되지 않은 코드와 어떻게든 작동해야 합니다. 예를 살펴보겠습니다:
List<CharSequence> lines = new ArrayList<String>();
우리가 기억하는 것처럼, 이러한 라인은 제네릭의 불변성으로 인해 컴파일되지 않습니다.
List<Object> lines = new ArrayList<String>();
그리고 이것도 같은 이유로 컴파일되지 않습니다.
List lines = new ArrayList<String>();
List<String> lines2 = new ArrayList();
이러한 줄은 컴파일되고 작동합니다. 원시 유형이 사용되는 것은 바로 그 것입니다. 지정되지 않은 유형. 다시 한 번, 최신 코드에서는 원시 유형을 사용해서는 안 된다는 점을 지적할 가치가 있습니다.
고양이용 제네릭 - 5

유형화된 클래스

그래서 수업을 입력했습니다. 우리 자신의 타입 클래스를 어떻게 작성할 수 있는지 살펴보겠습니다. 예를 들어, 클래스 계층 구조가 있습니다:
public static abstract class Animal {
  public abstract void voice();
}

public static class Cat extends Animal {
  public void voice(){
    System.out.println("Meow meow");
  }
}

public static class Dog extends Animal {
  public void voice(){
    System.out.println("Woof woof");
  }
}
우리는 동물 컨테이너를 구현하는 클래스를 만들고 싶습니다. 어떤 Animal. 이것은 간단하고 이해할 수 있지만... 개와 고양이를 섞는 것은 나쁘고 서로 친구가 아닙니다. 또한 누군가가 그러한 용기를 받으면 실수로 용기에서 고양이를 개 무리에 던질 수 있으며 이는 아무런 소용이 없습니다. 그리고 여기서 제네릭이 우리를 도울 것입니다. 예를 들어 다음과 같이 구현을 작성해 보겠습니다.
public static class Box<T> {
  List<T> slots = new ArrayList<>();
  public List<T> getSlots() {
    return slots;
  }
}
우리 클래스는 T라는 제네릭으로 지정된 유형의 객체와 함께 작동합니다. 이것은 일종의 별칭입니다. 왜냐하면 제네릭은 클래스 이름에 지정되며 클래스를 선언할 때 이를 받게 됩니다.
public static void main(String[] args) {
  Box<Cat> catBox = new Box<>();
  Cat murzik = new Cat();
  catBox.getSlots().add(murzik);
}
보시다시피, Box에서만 작동하는 가 있음을 표시했습니다 Cat. 컴파일러는 catBox제네릭 대신 제네릭의 이름이 지정된 곳마다 T유형을 대체해야 한다는 것을 깨달았습니다 . CatT
고양이용 제네릭 - 6
저것들. 실제로 무엇이 되어야 하는지 Box<Cat>이해하는 것은 컴파일러 덕분입니다 . 내부 에는 , 가 포함 됩니다 . 유형 선언에는 여러 제네릭이 있을 수 있습니다. 예를 들면 다음과 같습니다. slotsList<Cat>Box<Dog>slotsList<Dog>
public static class Box<T, V> {
제네릭의 이름은 무엇이든 될 수 있지만, 일부 암묵적인 규칙을 따르는 것이 좋습니다. - "유형 매개변수 명명 규칙": 요소 유형 - E, 키 유형 - K, 숫자 유형 - N, T - 유형, V - 값 유형. 그건 그렇고, 제네릭은 불변적이라고 말한 것을 기억하십시오. 상속 계층 구조를 유지하지 마십시오. 실제로 우리는 이에 영향을 미칠 수 있습니다. 즉, 우리는 제네릭을 COvariant로 만들 기회가 있습니다. 상속을 동일한 순서로 유지합니다. 이 동작을 "제한된 유형"이라고 합니다. 제한된 유형. 예를 들어, 우리 클래스에는 Box모든 동물이 포함될 수 있으며 다음과 같은 제네릭을 선언합니다.
public static class Box<T extends Animal> {
즉, 클래스에 상한선을 설정합니다 Animal. 키워드 뒤에 여러 유형을 지정할 수도 있습니다 extends. 이는 우리가 작업할 유형이 일부 클래스의 자손이어야 하며 동시에 일부 인터페이스를 구현해야 함을 의미합니다. 예를 들어:
public static class Box<T extends Animal & Comparable> {
이 경우 Box상속자가 아니고 Animal구현하지 않는 항목을 넣으려고 하면 Comparable컴파일 중에 오류가 발생합니다.
error: type argument Cat is not within bounds of type-variable T
고양이용 제네릭 - 7

메소드 타이핑

제네릭은 유형뿐만 아니라 개별 메서드에도 사용됩니다. 메소드의 적용은 공식 튜토리얼 " Generics Methods "에서 볼 수 있습니다.

배경:

고양이용 제네릭 - 8
이 사진을 봅시다. 보시다시피, 컴파일러는 메소드 서명을 보고 정의되지 않은 클래스를 입력으로 사용하고 있음을 확인합니다. 우리가 어떤 종류의 객체를 반환한다는 것을 서명으로 결정하지 않습니다. 물체. 따라서 ArrayList를 생성하려면 다음을 수행해야 합니다.
ArrayList<String> object = (ArrayList<String>) createObject(ArrayList.class);
출력이 ArrayList가 될 것이라고 명시적으로 작성해야 합니다. 이는 보기 흉하고 실수할 가능성이 추가됩니다. 예를 들어, 이런 넌센스를 작성하면 다음과 같이 컴파일됩니다.
ArrayList object = (ArrayList) createObject(LinkedList.class);
컴파일러를 도와드릴까요? 예, 제네릭을 사용하면 이를 수행할 수 있습니다. 동일한 예를 살펴보겠습니다.
고양이용 제네릭 - 9
그런 다음 다음과 같이 간단하게 객체를 만들 수 있습니다.
ArrayList<String> object = createObject(ArrayList.class);
고양이용 제네릭 - 10

와일드카드

Oracle의 Generics Tutorial, 특히 " Wildcards " 섹션에 따르면 물음표라고 하는 물음표를 사용하여 "알 수 없는 유형"을 설명할 수 있습니다. 와일드카드는 제네릭의 일부 제한 사항을 완화하는 편리한 도구입니다. 예를 들어 앞서 논의한 것처럼 제네릭은 불변입니다. 즉, 모든 클래스는 Object 유형의 하위 유형(하위 유형)이지만 List<любой тип>하위 유형은 아닙니다 List<Object>. 그러나 List<любой тип>그것은 하위 유형입니다 List<?>. 따라서 다음 코드를 작성할 수 있습니다.
public static void printList(List<?> list) {
  for (Object elem: list) {
    System.out.print(elem + " ");
  }
  System.out.println();
}
일반 제네릭(즉, 와일드카드를 사용하지 않는)과 마찬가지로 와일드카드가 있는 제네릭은 제한될 수 있습니다. 상한 와일드카드는 친숙해 보입니다.
public static void printCatList(List<? extends Cat> list) {
  for (Cat cat: list) {
    System.out.print(cat + " ");
  }
  System.out.println();
}
그러나 하한 와일드카드를 사용하여 제한할 수도 있습니다.
public static void printCatList(List<? super Cat> list) {
따라서 이 메서드는 모든 고양이는 물론 더 높은 계층(객체까지)을 허용하기 시작합니다.
고양이용 제네릭 - 11

유형 삭제

제네릭에 관해 말하자면 "유형 지우기"에 대해 알아두는 것이 좋습니다. 실제로 유형 삭제는 제네릭이 컴파일러에 대한 정보라는 사실에 관한 것입니다. 프로그램 실행 중에는 제네릭에 대한 정보가 더 이상 존재하지 않습니다. 이를 "삭제"라고 합니다. 이 삭제는 일반 유형이 특정 유형으로 대체되는 효과를 갖습니다. 일반에 경계가 없으면 객체 유형이 대체됩니다. 테두리가 지정된 경우(예 <T extends Comparable>: ) 해당 테두리가 대체됩니다. 다음은 Oracle 튜토리얼: " Erasure of Generic Types "의 예입니다.
고양이용 제네릭 - 12
위에서 말한 것처럼 이 예에서는 일반이 T경계선까지 지워집니다. 전에 Comparable.
고양이용 제네릭 - 13

결론

제네릭은 매우 흥미로운 주제입니다. 이 주제가 당신의 관심을 끌기를 바랍니다. 요약하자면, 제네릭은 한편으로는 유형 안전성을 보장하고 다른 한편으로는 유연성을 보장하기 위해 컴파일러에 추가 정보를 제공하기 위해 개발자에게 제공되는 훌륭한 도구입니다. 관심이 있으시면 제가 좋아하는 리소스를 확인해 보시기 바랍니다. #비아체슬라프
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION