每日 Java 面试题分享【第 9 天】
欢迎来到每日 Java 面试题分享栏目!
订阅专栏,不错过每一天的练习
今日分享 3 道面试题目!
评论区复述一遍印象更深刻噢~
目录
- 问题一:Java 泛型的作用是什么?
- 问题二:Java 泛型擦除是什么?
- 问题三:什么是 Java 泛型的上下界限定符?
问题一:Java 泛型的作用是什么?
Java 泛型的作用
Java 泛型是 JDK 1.5 引入的一项重要特性,它主要用于类、接口和方法的定义中,以参数化的方式实现 类型安全 和 代码的重用性。以下是泛型的主要作用:
1. 提高类型安全
在使用泛型时,编译器会在编译阶段进行类型检查,从而避免了运行时出现 ClassCastException
的风险。
示例:未使用泛型
List list = new ArrayList();
list.add("Hello");
list.add(123); // 没有编译错误,但可能导致运行时异常
String str = (String) list.get(1); // ClassCastException
示例:使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译错误:不允许添加非 String 类型
String str = list.get(0); // 无需强制类型转换
2. 消除强制类型转换
在没有泛型的情况下,开发者需要频繁地进行强制类型转换,代码冗长且容易出错。使用泛型后,编译器自动推断类型,简化了代码书写。
示例:未使用泛型
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 强制类型转换
示例:使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 无需强制类型转换
3. 提高代码的复用性
泛型允许类、接口和方法可以操作任意类型的数据,从而增强代码的复用性。
示例:泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
String[] strArray = {"A", "B", "C"};
Integer[] intArray = {1, 2, 3};
printArray(strArray); // 适用于 String 数组
printArray(intArray); // 适用于 Integer 数组
}
4. 支持泛型约束
通过 extends
和 super
关键字,泛型可以对类型参数施加约束,从而限制泛型类型的使用范围。
示例:限定上界
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
System.out.println(sum(3, 5)); // 使用 Integer
System.out.println(sum(3.5, 4.5)); // 使用 Double
// sum("3", "5"); // 编译错误:String 不是 Number 的子类
}
5. 支持复杂的数据结构
泛型特别适合用来构建复杂的集合或数据结构,能更灵活地定义其存储的数据类型。
示例:嵌套泛型
Map<String, List<Integer>> map = new HashMap<>();
List<Integer> values = Arrays.asList(1, 2, 3);
map.put("Numbers", values);
System.out.println(map.get("Numbers")); // 输出: [1, 2, 3]
扩展讲解
1. 泛型的工作原理(类型擦除)
泛型在编译后会经过类型擦除,在字节码中,所有泛型信息都会被擦除,并替换为其限定类型或 Object
。这意味着泛型只存在于编译阶段,对运行时性能没有影响。
示例:
List<String> list = new ArrayList<>();
list.add("Hello");
// 编译后等价于:
List list = new ArrayList();
list.add("Hello");
2. 常见的泛型边界
T extends SomeClass
:指定类型参数必须是某个类的子类或实现某接口。T super SomeClass
:指定类型参数必须是某个类的父类。? extends SomeClass
:通配符表示某个类的子类,用于只读操作。? super SomeClass
:通配符表示某个类的父类,用于写操作。
面试技巧
- 描述核心作用:安全性(类型检查)、简洁性(省去强制类型转换)、复用性(通用代码)。
- 用代码说明:简单明了的代码示例让你的回答更具体。
- 扩展知识点:深入说明类型擦除、泛型边界等高频考察点。
- 实际应用场景:说明泛型在开发中如何提高生产力,如集合类的使用。
这样回答既全面又逻辑清晰,能给面试官留下深刻印象!
问题二:Java 泛型擦除是什么?
Java 泛型擦除
泛型擦除(Type Erasure) 是 Java 中一个重要的概念,是指 Java 在编译阶段会将所有泛型类型信息删除,并将其转换为原始类型。这个过程发生在编译时,目的是为了保证 Java 泛型的兼容性,即与原始类型(即未使用泛型的代码)兼容,并且允许泛型代码与旧代码共同使用。
1. 泛型擦除的工作原理
当你编写带有泛型的代码时,Java 编译器会在编译阶段使用 类型擦除 将泛型类型转换为原始类型。这意味着,泛型类型的信息在运行时是不保留的。所有泛型类型的实际类型都会被替换为它们的上界类型(如果有的话),如果没有上界,则替换为 Object
。
例如:
List<String> list = new ArrayList<>();
在编译后,List<String>
会被擦除为 List
,因此类型信息丢失,字节码中只有 List
,并没有 String
类型的信息。
2. 泛型擦除后的代码
以下是擦除后代码的例子:
示例:
public class Example<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
编译后,类型擦除的结果是这样的:
public class Example {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
解释:
- 泛型类型
T
被替换成了Object
。 - 方法的签名中
T
变成了Object
,这意味着你可以将任何类型的对象赋值给value
,但是你在获取时必须进行类型转换。
3. 泛型擦除的影响
- 不能获取泛型类型的真实类型:由于泛型信息在运行时丢失,因此不能通过反射获取泛型的真实类型。
示例:
public class Example<T> {
public void printType() {
System.out.println(T.class); // 编译错误:无法确定 T 的类型
}
}
- 泛型类型参数只能用于方法签名:泛型类型的存在仅限于方法参数、返回值等地方,类和字段中使用泛型时,实际上使用的是擦除后的类型。
4. 泛型擦除与类型边界
当泛型类型指定了上界时,擦除会将泛型类型替换为该上界类型。如果没有指定上界,则会替换为 Object
。
示例:
public class Example<T extends Number> {
public void printType() {
System.out.println(T.class); // 编译错误:无法获取 T 的类型
}
}
在编译后,T extends Number
会被擦除为 Number
,并且代码在运行时实际操作的是 Number
类型。
5. 泛型擦除与方法重载
因为类型擦除后泛型信息消失,Java 编译器并不能区分同名泛型方法,所以会导致编译错误。
示例:
public class Example {
public void printList(List<String> list) {
System.out.println("List of Strings");
}
public void printList(List<Integer> list) {
System.out.println("List of Integers");
}
}
编译时会出现错误,因为在泛型擦除后,两个方法的签名都是 printList(List)
,这会导致编译器无法区分它们。
6. 常见的泛型擦除问题
- 无法判断泛型类型:泛型擦除使得你不能在运行时使用反射来获取类型信息。
- 泛型与继承的关系:泛型类和普通类无法共享类型参数,因此不同的泛型类型是完全不同的类。
- 泛型的协变和逆变问题:因为擦除,无法使用不同类型的泛型进行转换。
扩展讲解
-
类型擦除的目的:泛型擦除的一个主要目的是兼容旧的 Java 代码。在泛型引入之前,Java 并没有泛型,因此泛型擦除确保了新的泛型代码与旧代码能够无缝兼容,不会影响 Java 语言的向后兼容性。
-
泛型擦除与反射:由于类型擦除的存在,Java 的反射机制无法获取泛型类型的信息。因此,如果你需要在运行时知道对象的实际类型,可以考虑使用
Type
或ParameterizedType
接口来获取更详细的信息。
面试技巧
- 掌握泛型擦除的细节:理解擦除如何影响类和方法,尤其是在反射和类型检查中。
- 强调类型安全:在回答时,强调泛型擦除的影响,但同时也强调泛型能带来的类型安全性和代码简化。
- 实际应用示例:使用代码示例展示擦除的行为,以及如何影响方法重载和反射操作。
通过对泛型擦除的深刻理解,你可以在面试中表现出对 Java 内部机制的透彻掌握,给面试官留下良好的印象!
问题三:什么是 Java 泛型的上下界限定符?
Java 泛型的上下界限定符
Java 泛型的上下界限定符(Upper Bound and Lower Bound)允许我们对泛型类型进行更精细的控制,限制其可以接受的类型范围。Java 提供了两种类型限定符:上界限定符(extends
)和下界限定符(super
),它们分别用于控制泛型类型的上限和下限。
1. 上界限定符(extends
)
上界限定符用于限定泛型类型参数必须是某个特定类或接口的子类,或者是该类本身。通过使用 extends
关键字,我们可以确保泛型类型的参数类型不会超过某个特定的上界类型。
语法
<T extends ClassName>
示例:
// 只允许泛型类型 T 是 Number 类或其子类
public class NumberPrinter<T extends Number> {
public void print(T number) {
System.out.println(number);
}
}
在上述代码中,T
的类型被限制为 Number
或者其子类(如 Integer
, Double
等),这样我们就保证了传入的类型至少是 Number
类型的某个子类。
应用场景:
- 当你希望确保泛型类型至少是某个类的子类时,使用上界限定符。
- 通常用于数学运算、集合操作等对类型有一定要求的场景。
2. 下界限定符(super
)
下界限定符用于限定泛型类型参数必须是某个特定类的父类,或者是该类本身。通过使用 super
关键字,泛型类型的参数只能是某个类或其父类。
语法
<T super ClassName>
示例:
// 只允许泛型类型 T 是 Number 类或其父类
public class NumberPrinter<T super Integer> {
public void print(T number) {
System.out.println(number);
}
}
在这个例子中,T
的类型被限制为 Integer
或者 Integer
的父类(比如 Number
或 Object
)。
应用场景:
- 当你希望泛型类型接受某个类及其父类时,使用下界限定符。
- 主要用于写入操作,确保可以向泛型类型传入某个类及其父类的对象。
3. 通配符与上下界结合使用(? extends T
和 ? super T
)
Java 泛型中的通配符 ?
可以和上界限定符 extends
或下界限定符 super
结合使用,用于表示不确定的类型。
上界通配符(? extends T
)
? extends T
表示泛型类型参数的上界是 T
,即该类型可以是 T
类型或其子类。
示例:
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
List<? extends Number>
可以接受List<Integer>
,List<Double>
,List<Float>
等类型。- 适用于只读操作,即从集合中读取数据。
下界通配符(? super T
)
? super T
表示泛型类型参数的下界是 T
,即该类型可以是 T
类型或其父类。
示例:
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 允许添加 Integer 类型的数据
}
List<? super Integer>
可以接受List<Integer>
,List<Number>
,List<Object>
等类型。- 适用于写入操作,即向集合中添加数据。
4. 上下界限定符的应用实例
示例 1:上界限定符
假设我们有一个泛型方法,该方法只能接受 Number
类或其子类作为参数:
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
public static void main(String[] args) {
System.out.println(sum(10, 20)); // 使用 Integer
System.out.println(sum(10.5, 20.5)); // 使用 Double
}
在这个示例中,T
被限制为 Number
或其子类(如 Integer
, Double
),确保了传入参数是数值类型。
示例 2:下界限定符
假设我们有一个方法,接受的参数是 Integer
或其父类(如 Number
):
public static <T super Integer> void printList(List<T> list) {
for (T element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
List<Number> list = new ArrayList<>();
list.add(10);
printList(list); // 适用于 Number 或其父类
}
在这个例子中,T
被限制为 Integer
或 Integer
的父类(Number
)。
扩展讲解
-
为什么要使用上下界限定符?
- 上界限定符让你可以限制泛型类型的上限,保证泛型类型参数符合特定要求。
- 下界限定符让你可以向某个类型或其父类写入数据,保证泛型方法的写入操作不违反类型规则。
-
应用场景
- 上界限定符用于只读操作,例如获取集合中的数据。
- 下界限定符用于写入操作,例如向集合中添加数据。
-
通配符与泛型结合:泛型和通配符的结合(如
? extends T
和? super T
)让我们可以编写更加灵活和安全的代码,支持更多的操作场景。
面试技巧
- 简洁明了地解释上下界限定符的作用,重点强调它们如何限制泛型类型的范围,保证类型安全。
- 举例说明:使用实际的代码示例来清晰地展示如何使用上下界限定符。
- 扩展知识:提及通配符的应用场景,帮助面试官看到你对泛型的深刻理解。
理解并掌握泛型上下界限定符,将使你在 Java 后端开发中写出更具类型安全性和可扩展性的代码,也能在面试中展现出扎实的基础知识!
总结
今天的 3 道 Java 面试题,您是否掌握了呢?持续关注我们的每日分享,深入学习 Java 面试的各个细节,快速提升技术能力!如果有任何疑问,欢迎在评论区留言,我们会第一时间解答!
明天见!🎉