java泛型常见面试题

背景:泛型这个知识点平时用的不多,但是在面试的时候很容就被问到,所以还是要准备一些基础的知识储备。

面试旧敌之 Java 泛型 :主要概念及特点

“泛型” 意味着编写的代码可以被不同类型的对象所重用。

泛型是在JDK1.5之后出现的。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

可以看到,使用 Object 来实现通用、不同类型的处理,有这么两个缺点:

  1. 每次使用时都需要强制转换成想要的类型
  2. 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全

根据《Java 编程思想》中的描述,泛型出现的动机在于:

有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类

实际上引入泛型的主要目标有以下几点:

    • 类型安全 
      • 泛型的主要目标是提高 Java 程序的类型安全
      • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
      • 符合越早出错代价越小原则
    • 消除强制类型转换 
      • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
      • 所得即所需,这使得代码更加可读,并且减少了出错机会
    • 潜在的性能收益 
      • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
      • 所有工作都在编译器中完成
      • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

泛型类 

泛型接口

泛型方法

泛型的通配符

无限制通配符

extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类

super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

通配符比较

通过上面的例子我们可以知道,无限制通配符 < ?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景。

两种有限制通配形式 < ? super E> 和 < ? extends E> 也比较容易混淆,我们再来比较下。

你了解泛型通配符与上下界吗?

ps:上下限用图说明

它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。

  • < ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
  • < ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。

用《Effective Java》 中的一个短语来加深理解:

为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限:

PECS: producer-extends, costumer-super

因此使用通配符的基本原则:

  • 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  • 如果它表示一个 T 的消费者,就使用 < ? super T>;
  • 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

小总结一下:

  • T 的生产者的意思就是结果会返回 T,这就要求返回一个具体的类型,必须有上限才够具体;
  • T 的消费者的意思是要操作 T,这就要求操作的容器要够大,所以容器需要是 T 的父类,即 super T;

泛型的类型擦除

Java 中的泛型和 C++ 中的模板有一个很大的不同:

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

(摘自:blog.youkuaiyun.com/fw0124/arti…

在 Java 中,泛型是 Java 编译器的概念,用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。

实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。

当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)

泛型的规则

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

泛型的使用场景

当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。

总结

1.上面说到使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优势,那为什么 ArrayList 等源码中的还能看到使用 Object 作为类型?

根据《Effective Java》中所述,这里涉及到一个 “移植兼容性”

泛型出现时,Java 平台即将进入它的第二个十年,在此之前已经存在了大量没有使用泛型的 Java 代码。人们认为让这些代码全部保持合法,并且能够与使用泛型的新代码互用,非常重要。

这样都是为了兼容,新代码里要使用泛型而不是原始类型。

2.泛型是通过擦除来实现的。因此泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码随意互用。

3.如果类型参数在方法声明中只出现一次,可以用通配符代替它。

比如下面的 swap 方法,用于交换指定 List 中的两个位置的元素:

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

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

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

对比一下,第二种更加简单清晰吧。

4.数组中不能使用泛型

这可能是 Java 泛型面试题中最简单的一个了,当然前提是你要知道 Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

5.Java 中 List 和原始类型 List 之间的区别?

原始类型和带参数类型 之间的主要区别是:

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

这道题的考察点在于对泛型中原始类型的正确理解。

ps:这个地方还没理解。。。。

转载于:https://www.cnblogs.com/lixuwu/p/10829368.html

### Java 面试题及解答 #### 1. 编译器要将擦除,为什么还需要使用? 尽管编译器会执行类擦除操作,即在运行时移除所有的信息并替换为 `Object` 类,但这并不意味着可以简单地用 `Object` 来替代的主要优势在于提供更强的类安全性和更好的可读性。 - **类安全性**:可以在编译期捕获类错误,减少运行时异常的发生。例如,在不使用的情况下,向集合中添加不同类对象不会引发任何警告;而在使用后,则会在尝试插入不符合类的元素时报错[^1]。 - **代码简洁度与易维护性**:利用可以使代码更加通用化和模块化,从而提高开发效率以及降低后期维护成本。比如定义一个工具函数处理多种数据结构而不必针对每种情况单独编写版本。 ```java // 使用 Object 的例子 List list = new ArrayList(); list.add("string"); String s = (String) list.get(0); // 这里可能会抛出 ClassCastException 异常 // 使用的例子 List<String> stringList = new ArrayList<>(); stringList.add("string"); String str = stringList.get(0); // 不需要强制转换, 更加安全可靠 ``` #### 2. 如何理解Java中的通配符? Java 中引入了两种主要形式的通配符——无界通配符 (`?`) 和有界通配符 (`<? extends T>` 或 `<? super T>`): - **无界通配符**:表示未知的具体类,通常用于只读场景下声明容器变量; - **上界通配符**:指定某个特定父类及其子类作为实际参数类范围; - **下界通配符**:限定具体类必须是给定类或其祖先之一。 这些机制使得我们可以写出更灵活且健壮的应用程序逻辑,同时也增强了 API 设计上的表达能力。 ```java public static void printCollection(Collection<?> c){ for(Object e : c){ System.out.print(e + " "); } } ``` #### 3. 方法如何定义? 除了类级别的外,还可以创建独立于实例存在的静态成员方法,并为其赋予自己的类形参列表。这被称为“方法”。语法如下所示: ```java <T> 返回值类 方法名(参数列表); ``` 其中 `<T>` 表明这是一个带有单个类参数的方法签名。如果存在多个这样的占位符,则它们之间需要用逗号分隔开。 下面是一个简单的示例展示了如何实现一个接受任意两个同质元素并返回较大者比较器接口实现方式: ```java public class Util { public static <T extends Comparable<T>> T max(T a, T b){ return a.compareTo(b)>0 ? a:b; } } // 调用此方法的方式如下: Integer m = Util.max(5, 9); Double n = Util.max(3.14, 2.718); ``` 以上就是一些常见的有关 Java 方面的知识点总结,希望对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值