Java基础--泛型(最全泛型总结)(泛型面试)

1,泛型是什么

“泛型”意味着编写的代码可以被不同类型的对象所重用。泛型的提出是为了编写出重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数

2,为什么引入泛型,为什么用泛型

在没有引入泛型之间,如何想要实现一个通用的,可以处理不同类型的方法,我们需要使用Object类作为属相和方法参数,例如:

class Generic{
    private Object[] GData;
    public Generic(int capacity){
        GData = new Object[capacity];
    }
    public Object getGData(int index) {
        return GData[index];
    }
    public void add(int index,Object item) {
        GData[index] = item;
    }
}

这样使用一个Object数组来保存数据,这样在使用时可以添加不同类型的对象**:**

		Generic generic = new Generic(10);
        generic.add(0,"泛型");
        generic.add(1,666);

因为Object是所有类的父类,所以所有的类都可以作为成员被添加到该类中,需要时需要进行强制转型,但是转型可能出现转型异常:

		String item1 = (String) generic.getGData(0);
        String item2 = (String) generic.getGData(1); //java.lang.ClassCastException

通过这里我们可以知道,使用Object实现普适的不同类型的处理,会有以下两个缺点:
🐖:每次使用时需要强制转换类型,让其转换成我们想要的类型。
🐖:编译器不知道类型转换是否正常,所以编译器不报错,运行期才报错,不安全。

***JDK1.5之后:***泛型出现;那为什么引入泛型?也是泛型有什么好处?

🐕类型安全

🐏泛型的目标是提高Java程序的类型安全。
🐏将运行期报出来的ClassCastException,移到编译期,符合早出错代价越小原则。

🐕消除强制类型转换

🐏使用时直接得到目标类型,消除许多强制的类型转换
🐏所需即所得,使得代码更加可读,并且减少了出错机会

🐕潜在的性能收益

🐏由于泛型的实现方式,支持泛型几乎不需要JVM或类文件更改。(这里抛出一个问题即泛型的实现方式是什么样的?)
🐏所有的工作在编译器中完成
🐏编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已。

3,泛型的使用方式

泛型的本质时参数化类型,也就是说所操作的数据类型被指定为一个参数。类型参数的意义是告诉编译器这个集合中要存放的实例的类型,从而在添加其他类型时做出提示,在编译时就为类型安全做了保证。
参数类型可以用在泛型类,泛型接口,泛型方法

class Generic<E>{//泛型类
    private E GData;
    public Generic(E data){
        GData = data;
    }

    public E getGData(){//泛型方法
        return GData;
    }
    public void setGData(E data){//泛型方法
        GData = data;
    }
    public interface GenericInterface<E>{//泛型接口
        void doSomething(E e);
    }
}

🐖泛型类
泛型类和普通类的区别就是类名后面有类型参数列表< E > ,也可以有多个,例如class HashMap<K, V>,参数名称由开发者确定。
声明后内部成员,方法就可以使用这个参数类型,泛型最常见的用途是作为容器不同类型数据的容器类,比如Java集合容器类。
🐖泛型接口
和泛型类一样,泛型接口在接口名后添加类型参数,比如以下 GenericInterface< E >,接口声明类型后,接口方法就可以直接使用这个类型。
但要注意继承该接口的实现类要指明具体的参数类型,不然默认类型是Object,这样失去 了泛型接口的意义。

class Generic1 implements Generic.GenericInterface{
    //不指明实现类类型
    @Override
    public void doSomething(Object o) {
        
    }
}
class Generic2 implements Generic.GenericInterface<String>{
    //指明实现类类型为String
    @Override
    public void doSomething(String s) {
        
    }
}

泛型接口比较实用的使用场景就是用作策略模式的公共策略, Comparator就是一个泛型接口:

public interface Comparator< T >{
public int compare(T lhs, Trhs);
public bollean equals(Object object);}
泛型接口定义基本的规则,然后作为引用传递给客户端,这样在运行时就能传入不同的策略实现类。

🐖泛型方法
泛型方法是指使用泛型的方法,如果它所在的类是一个泛型类,那就简单了,直接使用类声明的参数。
如果一个方法所在的类不是泛型类,或者他想要处理不同于泛型类声明的类型数据,那它就需要自己声明类型。

class Person<E> {
    private E data;
    public E getData(E data){
        return data;
    }
    public <T> Set<T> union(Set<T> s1,Set<T> s2){
        Set<T> set = new HashSet<>(s1);
        set.addAll(s2);
        return set;
    }
}

4,泛型的通配符

通配符:给传入的类型指定一个范围,从而可以进行一些特定的操作。有三种通配符形式:
🐖:<?> 无限制通配符
🐖: <? extends E > extends关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
🐖: <? super E> super关键字声明了类型的下界,表明参数化类型可能是指定类型,或者是此类型的父类。

非常注意这里涉及到了Java的协变逆变知识

无限制通配符 < ?>

要使用泛型,但是不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。

? 和 Object 不一样,List<?> 表示未知类型的列表,而 List 表示任意类型的列表。

如传入个 List ,这时 List 的元素类型就是 String,想要往 List 里添加一个 Object,这当然是不可以的。

上界通配符 < ? extends E>

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

如果传入的类型不是 E 或者 E 的子类,编辑不成功
泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

下界通配符 < ? super E>

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

private <E> void add(List<? super E> dst, List<E> Src){
    for (E e : src){
    dst.add(e);
   }
}

通配符比较和使用
通配符extends和super的图文例详细介绍
< ? > 无限制和Object有些相似,用于表示无限制或者不确定的范围的场景。
< ? super E > 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型大的比较方法可以应用于子类对象。
< ? extends E >用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象。
总结使用原则:

  • 如果参数化类型表示一个E的生产者,使用< ? super E >;E的子类
  • 如果它表示一个E的消费者,就使用< ? extends E >;E的父类
  • 如果两者都是,那使用通配符就没有意义了,因为这时需要精确的参数类型。
  • T 的生产者的意思就是结果会返回 T,这就要求返回一个具体的类型,必须有上限才够具体;
  • T 的消费者的意思是要操作 T,这就要求操作的容器要够大,所以容器需要是 T 的父类,即 super T;
private <E extends Comparable<? super E>> E max(List<? extends E> e1){
    if(e1 == null){
        return null;
    }
    //迭代器返回的元素属于 E 的某个子类型
    Iterator<? extends E> iterator = e1.iterator();
    E result = iterator.next();
    while (iterator.hasNext()){
     E next = iterator.next();
      if(next.compareTo(result)>0){
       result = next;
      }
    }
    return result;
}

1. 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)
2. Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super
3. 而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大
extends 可用于的返回类型限定,不能用于参数类型限定。
super 可用于参数类型限定,不能用于返回类型限定。
希望只取出,不插入,就使用? extends
希望只插入,不取出,就使用? super
希望,即能插入,又能取出,就不要用通配符?

5,泛型的类型擦除

这里需要比较Java的泛型和C++中的模板的区别:

  • C++中模板在实例化时,会为每一个类型产生一套不同的模板,这就是所谓的代码膨胀。
  • Java中不会产生这个问题,虚拟机中并没有泛型类型对象,所有对象都是普通类。

Java中泛型是编译器的概念,泛型编写或普通的Java程序基本是相同的,只是多了一些参数化的类型,少了一些类型转换。
泛型程序先要被转换成一般的,不带泛型的Java程序后再进行处理,编译器自动完成从Generic Java到普通Java的翻译,Java虚拟机运行时对泛型基本一无所知。
当编译器对带有泛型的Java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的Java虚拟机接受并执行,这就叫做类型擦除。(type erasure)

		List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();
        System.out.println(strings.getClass() == integers.getClass());
        //ture
        System.out.println(strings.getClass());
        //class java.util.ArrayList
        System.out.println(integers.getClass());
        // class java.util.ArrayList

无论是否使用泛型,集合框架中存放的对象的数据类型都是Object,源码中可以看到,通过反射也可以看到,这原因就是泛型的擦除

6,擦除的实现原理

擦除会带来疑问,Java编译器在编译器擦除了类型的信息,那运行中怎么保证添加,取出的类型就是擦除前声明的呢?
编译器会将泛型代码中的类型完全擦除,使其变成原始类型。当然,这时的代码类型和我们想要的还不一样,接着编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,保证类型安全。

🐘擦除导致的泛型不可变性
泛型中没有逻辑上的父子关系

	/**
     * 这两个方法不是重载,编译不能通过
     * 报错both methods have same erasure,两个方法的擦除一样
     * void m(List number){}
     * void m(List String){}
     */
    void m(List<Object> number){}
     void m(List<String> String){}

泛型的这种情况称为 不可变性,与之对应的概念是 协变、逆变:

  • 协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List<
    B>)的父类,则称之为协变的(父子关系保持一致)
  • 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变(放入容器就篡位了)
  • 不可变:不论 A B 有什么关系,A 的容器和 B 的容器都没有父子关系,称之为不可变

Java 中数组是协变的,泛型是不可变的。

7,泛型的规则

  • 泛型的参数类型只能是类(包括自定义类),不能是简单类型。
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  • 泛型的类型参数可以有多个
  • 泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”
  • 泛型的参数类型还可以是通配符类型,例如 Class

8,总结

1.使用Object来达到复用,会失去泛型在安全性和直观表达性上的优势,那么为什么ArrayList等源码中还能看到使用Object作为类型?
泛型出现时,Java 平台即将进入它的第二个十年,在此之前已经存在了大量没有使用泛型的 Java 代码。人们为了让这些代码全部保持合法,并且能够与使用泛型的新代码互用,非常重要。这样都是为了兼容,但是我们新代码里要使用泛型而不是原始类型
2.泛型是通过擦除来实现的。因此泛型只在编译时强化它的类型信息,而在运行时擦除它的元素类型信息。擦除使得泛型的代码可以和没有使用泛型的代码随意互用。
3.如果类型参数在方法声明中只出现不止一次,可以用通配符代替它。

private <E> void swap(List<E> list, int i, int j){
    //....
}

只出现了一次 类型参数,没有必要声明,完全可以用通配符代替:

private void swap(List<?> list, int i, int j){
    //...
}

4.数组中不能使用泛型
Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
5.Java 中 List 和原始类型 List 之间的区别?

  • 在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查
  • 通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String 或 Integer
  • 你可以把任何带参数的类型传递给原始类型 List,但 却不能把 List< String> 传递给接受 List< Object>
    的方法,因为泛型的不可变性,会产生编译错误。
		List<Object> list = new ArrayList<>();
        list.add(new String());
        list.add(new StringBuffer());
        List<String> list1 = new ArrayList<>();
        list1.add("String");
        list = list1;//编译错误

9,补充

静态资源不认识泛型
**原因:**在java中泛型只是一个占位符,必须在传递类型后才能使用就泛型而言,类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了

泛型方法拥有以下的特征;
由作为参数声明的部分,此部分要放在参数返回值之前表明这是个泛型方法
泛型发放可以接受不同类型的参数,根据泛型方法的参数类型编译器适当处理每个方法的调用。当传入的类型不同就不同这样我们就可以通过传入的类型去实现同一段代码的重复使用了。

10,有关泛型的面试题

  1. Java中的泛型是什么 ? 使用泛型的好处是什么?
  2. Java的泛型是如何工作的 ? 什么是类型擦除 ?
  3. 什么是泛型中的限定通配符和非限定通配符 ?
  4. List<? extends T>和List <? super T>之间有什么区别 ?
  5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
  6. Java中如何使用泛型编写带有参数的类?
  7. 编写一段泛型程序来实现LRU缓存?
  8. 你可以把List传递给一个接受List参数的方法吗?
  9. Array中可以用泛型吗?
  10. 如何阻止Java中的类型未检查的警告?

参考答案1
参考答案2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值