>  기사  >  Java  >  자바 제네릭 소개

자바 제네릭 소개

伊谢尔伦
伊谢尔伦원래의
2017-01-24 14:57:501384검색

1. 제네릭 개념의 도입(제네릭이 필요한 이유)

먼저 다음 짧은 코드를 살펴보겠습니다.

public class GenericTest {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // 1
            System.out.println("name:" + name);
        }
    }
}

는 List 유형 컬렉션을 정의하고, 먼저 두 개의 문자열 유형 값을 추가한 다음 값을 추가합니다. 정수 유형입니다. 목록의 기본 유형이 개체이기 때문에 이는 완전히 허용됩니다. 후속 루프에서는 이전에 목록에 Integer 유형 값을 추가하는 것을 잊어버렸거나 다른 인코딩 이유로 인해 //1과 유사한 오류가 쉽게 발생할 수 있습니다. 컴파일 단계는 정상인데 런타임 중에 "java.lang.ClassCastException" 예외가 발생하기 때문입니다. 따라서 코딩 중에 이러한 오류를 감지하기가 어렵습니다.

Java SE 1.5 이전에는 제네릭이 없을 때 "임의" 매개변수가 Object 유형에 대한 참조를 통해 구현되었습니다. "임의"의 단점은 명시적인 유형 변환이 필요하다는 점이었습니다. 변환을 수행하려면 개발자가 실제 매개변수 유형을 미리 알아야 합니다. 강제 유형 변환 오류의 경우 컴파일러에서 오류 메시지를 표시하지 않을 수 있으며 런타임 중에 예외가 발생합니다. 이는 보안상 위험합니다. 제네릭의 장점은 컴파일 중에 유형 안전성이 확인되고 모든 캐스트가 자동으로 암시적으로 수행되어 코드 재사용이 향상된다는 것입니다.

2. 제네릭이란 무엇입니까?

제네릭은 Java SE 1.5의 새로운 기능입니다. 제네릭의 본질은 매개변수화된 유형입니다. 즉, 연산되는 데이터 유형이 매개변수로 지정됩니다. 이 매개변수 유형은 각각 일반 클래스, 일반 인터페이스 및 일반 메소드라고 하는 클래스, 인터페이스 및 메소드 작성에 사용될 수 있습니다. 그렇다면 매개변수화된 유형을 어떻게 이해합니까? 이름에서 알 수 있듯이 타입은 메소드의 가변 매개변수와 유사하게 원래의 특정 유형에서 매개변수화됩니다. 이때 유형도 매개변수(유형 매개변수라고 할 수 있음) 형식으로 정의되며, 그 다음에는 유형이 정의됩니다. 유형(유형 인수)을 사용/호출할 때 특정 유형이 전달됩니다.

먼저 위의 예시를 작성하는 일반적인 방법을 살펴보겠습니다.

public class GenericTest {
    public static void main(String[] args) {
        /*
        List list = new ArrayList();
        list.add("qqyumidi");
        list.add("corn");
        list.add(100);
        */
        List<String> list = new ArrayList<String>();
        list.add("qqyumidi");
        list.add("corn");
        //list.add(100);   // 1  提示编译错误
        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i); // 2
            System.out.println("name:" + name);
        }
    }
}

일반 쓰기를 사용한 후 //1에 Integer 유형의 객체를 추가하려고 하면 컴파일 오류가 발생합니다. List을 통해서는 String 유형만 포함할 수 있는 목록 컬렉션으로 직접 제한됩니다. elements.이므로 //2에서 강제 유형 변환을 수행할 필요가 없습니다. 왜냐하면 이때 컬렉션은 요소의 유형 정보를 기억할 수 있고 컴파일러는 이미 String 유형임을 확인할 수 있기 때문입니다.

위의 일반 정의와 결합하면 List에서 String이 실제 매개변수 유형이라는 것을 알 수 있습니다. 즉, 해당 List 인터페이스에는 유형 매개변수가 포함되어야 합니다. 그리고 get() 메소드의 반환 결과도 바로 이 형식 매개변수의 유형입니다(즉, 해당 수신 유형 실제 매개변수). List 인터페이스의 구체적인 정의를 살펴보겠습니다.

public interface List<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean addAll(int index, Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    List<E> subList(int fromIndex, int toIndex);
}

List 인터페이스에서 일반 정의를 채택한 후 1a4db2c2c2313771e5742b6debf617a1의 E가 유형 매개변수를 나타내고 특정 유형을 수신할 수 있음을 알 수 있습니다. 인수이며 이 인터페이스 정의에서 E가 나타날 때마다 외부에서 수신된 동일한 형식 인수를 나타냅니다.

당연히 ArrayList는 List 인터페이스의 구현 클래스이고 정의 형식은 다음과 같습니다.

public class ArrayList<E> extends AbstractList<E> 
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return ArrayList.this.elementData(offset + index);
    }
    
    //...省略掉其他具体的定义过程
}

이를 통해 소스 코드 관점에서 추가 시 컴파일 오류가 발생하는 이유를 알 수 있습니다. //1 의 Integer 유형 객체이고 //2 의 get() 으로 얻은 유형은 바로 String 유형입니다.

3. 튜플 클래스 라이브러리란 무엇이며 어떻게 사용하나요?

튜플을 사용하는 이유는 무엇인가요?

튜플은 목록과 마찬가지로 데이터 저장에 사용될 수 있으며 여러 데이터를 포함할 수 있지만 목록과 다릅니다. 목록은 동일한 데이터 유형만 저장할 수 있지만 튜플은 다릅니다. 다음과 같은 다양한 데이터 유형을 저장할 수 있습니다. int, string, list 등을 동시에 저장할 수 있으며 필요에 따라 무한 확장이 가능합니다.

예를 들어, 웹 애플리케이션에서 자주 발생하는 문제는 데이터 페이징입니다. 쿼리 페이징에는 여러 정보가 포함되어야 합니다. 즉, 쿼리 결과는 데이터를 반환합니다. 현재 페이지, 그러나 현재 페이지, 페이지 크기, 총 페이지 수 및 기타 정보를 전경에 표시해야 하는 경우 총 데이터 레코드 수와 같은 다른 정보가 있어야 하며 총 페이지 수 및 기타 정보를 계산해야 합니다. 위 정보를 바탕으로 한 정보입니다. 이때 특정 정보 페이지를 질의할 때 두 가지 데이터 유형이 반환되어야 하는데, 하나는 list(현재 데이터 레코드)이고 다른 하나는 int(총 레코드 수)입니다. 물론 이 두 값은 두 가지 방법과 두 가지 데이터베이스 연결을 통해 얻을 수 있다. 실제로 목록을 조회할 때 전체 레코드 수는 SQL 쿼리를 통해 얻은 것인데, 전체 레코드 수를 조회하기 위해 다른 메소드를 열고 다른 데이터베이스 연결을 만든다면 조금은 불필요하고 시간 낭비가 될 것입니다. 코드 낭비이자 삶의 낭비입니다. 심각한 말씀입니다~ 이 경우 튜플을 사용하여 하나의 데이터베이스 연결에서 총 레코드 수와 현재 페이지 레코드를 가져와서 간단하고 명확하게 저장할 수 있습니다!

4. 사용자 정의된 일반 인터페이스, 일반 클래스 및 일반 메서드

从上面的内容中,大家已经明白了泛型的具体运作过程。也知道了接口、类和方法也都可以使用泛型去定义,以及相应的使用。是的,在具体使用时,可以分为泛型接口、泛型类和泛型方法。

自定义泛型接口、泛型类和泛型方法与上述Java源码中的List、ArrayList类似。如下,我们看一个最简单的泛型类和方法定义:

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        System.out.println("name:" + name.getData());
    }
}
class Box<T> {
    private T data;
    public Box() {
    }
    public Box(T data) {
        this.data = data;
    }
    public T getData() {
        return data;
    }
}

在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
        System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
        System.out.println(name.getClass() == age.getClass());    // true
    }
}

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

五. 类型通配符

接着上面的结论,我们知道,Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a是否可以看成具有父子关系的泛型类型呢?

为了弄清这个问题,我们继续看下下面这个例子:

public class GenericTest {
    public static void main(String[] args) {
        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);
        getData(name);
        
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1
    }
    
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }
}

我们发现,在代码//1处出现了错误提示信息:The method getData(Boxc8f01a3f8889dcf657849dd45bc0fc4c) in the t ype GenericTest is not applicable for the arguments (Boxc0f559cc8d56b43654fcbe4aa9df7b4a)。显然,通过提示信息,我们知道Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。那么,原因何在呢?

public class GenericTest {
    public static void main(String[] args) {
        Box<Integer> a = new Box<Integer>(712);
        Box<Number> b = a;  // 1
        Box<Float> f = new Box<Float>(3.14f);
        b.setData(f);        // 2
    }
    public static void getData(Box<Number> data) {
        System.out.println("data :" + data.getData());
    }
}
class Box<T> {
    private T data;
    public Box() {
    }
    public Box(T data) {
        setData(data);
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。

假设Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上可以视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Boxc8f01a3f8889dcf657849dd45bc0fc4c不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。

好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Boxc0f559cc8d56b43654fcbe4aa9df7b4a和Boxc8f01a3f8889dcf657849dd45bc0fc4c的父类的一个引用类型,由此,类型通配符应运而生。

类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box6b3d0130bba23ae47fe2b8e8cddf0195在逻辑上是Boxc0f559cc8d56b43654fcbe4aa9df7b4a、Boxc8f01a3f8889dcf657849dd45bc0fc4c...等所有Boxf4669da5dd7ec4cbc793dba728d85fa1的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);
        getData(name);
        getData(age);
        getData(number);
    }
    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
}

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?

在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        Box<Number> number = new Box<Number>(314);
        getData(name);
        getData(age);
        getData(number);
        
        //getUpperNumberData(name); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }
    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
    
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }
}

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

类型通配符上限通过形如Boxa2b037db85f4e1df0e812b9647ac55a8形式定义,相对应的,类型通配符下限为Boxda50108ad159903fabe211f1543600e8形式,其含义与类型通配符上限正好相反

六. 怎么构建复杂模型如list元组?

泛型的一个重要好处是能够简单而安全地创建复杂的模型。如List元组。

package Generics;  
  
import java.util.ArrayList;  
  
class ThreeTuple2<A,B,C>{  
    public final A first;  
    public final B second;  
    private final C three;  
    public ThreeTuple2(A a,B b,C c){  
        first = a;  
        second = b;  
        three = c;  
    }  
    public String toString(){  
        return "(" + first + "," + second + "," + three + ")";  
    }  
}  
  
public class TupleList<A,B,C> extends ArrayList<ThreeTuple2<A,B,C>> {  
    static ThreeTuple2<Integer,String,Character> h(){  
        return new ThreeTuple2<Integer,String,Character>(99,"掌上洪城",&#39;a&#39;);  
    }  
    public static void main(String[] args) {  
        TupleList<Integer,String,Character> ts = new TupleList<Integer,String,Character>();  
        ts.add(h());  
        ts.add(h());  
        for(ThreeTuple2<Integer,String,Character> ttp:ts)  
        System.out.println(ttp);          
    }  
}  
package Generics;  
  
import java.util.ArrayList;  
  
class ThreeTuple2<A,B,C>{  
    public final A first;  
    public final B second;  
    private final C three;  
    public ThreeTuple2(A a,B b,C c){  
        first = a;  
        second = b;  
        three = c;  
    }  
    public String toString(){  
        return "(" + first + "," + second + "," + three + ")";  
    }  
}  
  
public class TupleList<A,B,C> extends ArrayList<ThreeTuple2<A,B,C>> {  
    static ThreeTuple2<Integer,String,Character> h(){  
        return new ThreeTuple2<Integer,String,Character>(99,"掌上洪城",&#39;a&#39;);  
    }  
    public static void main(String[] args) {  
        TupleList<Integer,String,Character> ts = new TupleList<Integer,String,Character>();  
        ts.add(h());  
        ts.add(h());  
        for(ThreeTuple2<Integer,String,Character> ttp:ts)  
        System.out.println(ttp);          
    }  
}  
/* 输出结果为: 
(99,掌上洪城,a) 
(99,掌上洪城,a) 
*/

七. 泛型的擦除

package generics;  
  
import java.util.*;  
  
public class ErasedTypeEquivalence {  
    public static void main(String[] args) {  
        Class c1 = new ArrayList<String>().getClass();  
        Class c2 = new ArrayList<Integer>().getClass();  
        System.out.println(c1 == c2);  
    }  
} /* 
     * Output: true 
     */// :~

在泛型内部,无法获得任何有关泛型参数类型的信息。

ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4和ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a是相同的类型。

擦除的补偿

要想在表达式中使用类型,需要显式地传递类型的class对象。

package generics;  
class Building {  
}  
  
class House extends Building {  
}  
  
public class ClassTypeCapture<T> {  
    Class<T> kind;  
  
    public ClassTypeCapture(Class<T> kind) {  
        this.kind = kind;  
    }  
  
    public boolean f(Object arg) {  
        return kind.isInstance(arg);  
    }  
  
    public static void main(String[] args) {  
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);  
        System.out.println(ctt1.f(new Building()));  
        System.out.println(ctt1.f(new House()));  
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);  
        System.out.println(ctt2.f(new Building()));  
        System.out.println(ctt2.f(new House()));  
    }  
} /* 
     * Output: true true false true 
     */// :~

八. 可以创建泛型数组吗?相应的应用场景怎么处理?

正如你在下面示例Erased.java中所见,不能创建泛型数组。一般的解决方案是任何想要创建泛型数组的地方都使用ArrayList:

package generics;  
  
public class Erased<T> {  
    private final int SIZE = 100;  
  
    public static void f(Object arg) {  
        if (arg instanceof T) {  
        } // Cannot make a static reference to the non-static type T  
        T var = new T(); // Error  
        T[] array = new T[SIZE]; // Error  
        T[] array = (T) new Object[SIZE]; // Unchecked warning  
    }  
} /// :~

使用ArrayList示例

package generics;  
  
import java.util.*;  
  
public class ListOfGenerics<T> {  
    private List<T> array = new ArrayList<T>();  
  
    public void add(T item) {  
        array.add(item);  
    }  
  
    public T get(int index) {  
        return array.get(index);  
    }  
} /// :~

九. 泛型限定(上限和下限)的表达式是怎样的?

上限:?extends E:可以接收E类型或者E的子类型对象。

下限:?super E:可以接收E类型或者E的父类型对象。

上限什么时候用:往集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。为什么?因为取的时候,E类型既可以接收E类对象,又可以接收E的子类型对象。 

下限什么时候用:当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。

十. 什么时候用泛型?

当接口、类及方法中的操作的引用数据类型不确定的时候,以前用的Object来进行扩展的,现在可以用泛型来表示。这样可以避免强转的麻烦,而且将运行问题转移到的编译时期。

泛型的细节:

1)、泛型到底代表什么类型取决于调用者传入的类型,如果没传,默认是Object类型;

2)、使用带泛型的类创建对象时,等式两边指定的泛型必须一致;

    原因:编译器检查对象调用方法时只看变量,然而程序运行期间调用方法时就要考虑对象具体类型了;

3)、等式两边可以在任意一边使用泛型,在另一边不使用(考虑向后兼容);

ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4al = new ArrayLista87fdacec66f0909fc0757c19f2d2b1d();  //错

//要保证左右两边的泛型具体类型一致就可以了,这样不容易出错。

ArrayListd876df6b879603b519fa77c3a7ff60d2 al = new ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4();

al.add("aa");  //错

//因为集合具体对象中既可存储String,也可以存储Object的其他子类,所以添加具体的类型对象不合适,类型检查会出现安全问题。 ?extendsObject 代表Object的子类型不确定,怎么能添加具体类型的对象呢?

public static voidmethod(ArrayLista559c2fd4cc43ceac6831fa3de4b0d38 al) {

al.add("abc");  //错

  //只能对al集合中的元素调用Object类中的方法,具体子类型的方法都不能用,因为子类型不确定。

十一. Java类库中的泛型有那些?

所有的标准集合接口都是泛型化的—— Collectiond94943c0b4933ad8cac500132f64757f、Listd94943c0b4933ad8cac500132f64757f、Setd94943c0b4933ad8cac500132f64757f 和 Mapb77a8d9c3c319e50d4b02a976b347910。类似地,集合接口的实现都是用相同类型参数泛型化的,所以HashMapb77a8d9c3c319e50d4b02a976b347910 实现 Mapb77a8d9c3c319e50d4b02a976b347910 等。

除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。





성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.