目录
2.java中,finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch
4.为什么 泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法,而不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
5.在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常。
1.静态代码块,代码块,构造方法执行顺序?
答:父类静态代码块——>子类静态代码块——>父类代码块——>父类构造方法——>子类代码块——>子类构造方法。
2.java中,finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch
在Java中,finally块用于确保无论是否发生异常,都会执行某些清理操作,比如关闭打开的文件流、数据库连接或网络连接等资源。遵循“在finally块中关闭资源”的最佳实践,可以防止资源泄露,保持程序的健壮性。下面通过一个文件操作的例子来说明这一原则:
BufferedReader reader = null;
try {
// 假设这是打开文件的操作,可能会抛出FileNotFoundException
reader = new BufferedReader(new FileReader("path/to/file.txt"));
// 读取文件内容...
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
// 处理文件未找到异常
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
// 处理其他I/O异常
System.err.println("Error reading file: " + e.getMessage());
} finally {
// 确保文件流被关闭
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// 这里也可以选择处理或记录关闭时发生的异常
System.err.println("Error closing file: " + e.getMessage());
}
}
}
在上述代码中,finally块确保了BufferedReader对象(即资源对象)在读取操作完成后总是被关闭,即便是在读取过程中发生了异常。注意,在finally块内关闭资源时还嵌套了一个try-catch结构,这是因为关闭资源的操作本身也可能抛出异常,需要妥善处理这些异常,以免它们掩盖了更上层的异常信息。
从Java 7开始的try-with-resources语句:
Java 7引入了try-with-resources语句,它自动管理资源,简化了资源关闭的代码,使得代码更加简洁和安全。在这个版本中,你不再需要显式地在finally块中关闭资源。当你声明资源(如BufferedReader reader)时,在try语句的大括号内,Java会自动调用该资源的close()方法,即使发生了异常也是如此。这种方式让代码更加清晰,减少了出错的可能性。
try (BufferedReader reader = new BufferedReader(new FileReader("path/to/file.txt"))) {
// 读取文件内容...
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
3.为什么使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添
加元素操作,否则会抛出 UnsupportedOperationException 异常
在Java中,Map接口提供了keySet()、values()和entrySet()方法来分别获取与Map关联的键集、值集以及键值对集。这些方法返回的集合视图(View)是Map内部数据的反映,它们设计为不可修改的,主要出于以下几点原因:
-
保持同步性:Map与它返回的集合视图之间需要保持同步性。如果直接修改通过keySet()、values()或entrySet()获得的集合(例如添加或删除元素),可能会破坏Map内部的数据结构和一致性。例如,如果允许直接在keySet()返回的集合中添加一个新键,而这个键并没有对应值映射关系存在于原Map中,就会导致不一致。
-
设计原则:根据Java集合框架的设计原则,返回的集合视图应当反映源集合的状态。如果允许修改这些视图(例如添加或删除元素),将难以确保所有的约束条件得到满足,特别是对于那些有特定约束的Map实现(如SortedMap、ConcurrentMap等)。
-
明确职责:Map接口本身已经提供了添加(put)、删除(remove)等操作键值对的方法。通过视图进行元素的添加或删除操作会混淆职责,使得代码难以理解和维护。设计上鼓励直接通过Map接口进行这类操作,以保证操作的明确性和安全性。
因此,为了保证数据的完整性和一致性,以及遵循良好的设计原则,当尝试对通过keySet()、values()或entrySet()获得的集合直接进行添加或删除操作时,Java集合框架选择抛出UnsupportedOperationException
异常,明确告知开发者这样的操作是不被支持的。
如果你确实需要基于这些集合视图进行修改操作,通常的做法是先通过这些方法获得集合的副本(如使用new HashSet<>(map.keySet())获得keySet的副本),然后在副本上进行操作。这样既不会影响到原始Map的状态,也符合集合框架的设计意图。
4.为什么 泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,
而<? super T>不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
泛型中的通配符<? extends T>
和<? super T>
是Java泛型的高级用法,它们分别表示了类型的上限和下限约束。
<? extends T>(协变)
当你声明一个类型为<? extends T>
时,这意味着你可以传入任何T的子类作为泛型参数,但这个集合只能用于读取(例如get操作),不能用于写入(例如add操作)。这是因为编译器无法确定具体是什么类型的子类实例会被添加到集合中,这可能导致类型安全问题。
List<? extends Number> list = new ArrayList<Integer>(); // 可以是一个Integer的列表或者其他Number子类的列表
Number num = list.get(0); // 允许,因为可以确定get出来的一定是Number或者其子类的实例
list.add(new Integer(1)); // 不允许,因为list可能是Number的其他子类列表,比如Double,添加Integer不安全
<? super T>(逆变)
而对于<? super T>
,它可以接受T或者T的父类作为泛型参数,意味着你可以向集合中添加T或T的子类实例,但是从集合中取出元素时,只能以Object类型(或声明的上限类型)接收,失去了泛型带来的类型安全检查。
例子:Java
1List<? super Integer> list = new ArrayList<Number>(); // 可以是一个Number的列表或者其他Integer超类的列表
2list.add(new Integer(1)); // 允许,因为Integer是Number的子类,符合要求
3Number num = list.get(0); // 只能以Number或更通用的Object类型接收,因为不确定list具体是什么类型
容易出错的场景
- 当使用
<? extends T>
时,尝试添加元素会报错,因为编译器无法保证添加的元素类型与集合中已有类型兼容。 - 使用
<? super T>
时,虽然可以添加T及其子类的元素,但获取元素时只能以最宽泛的类型(通常是Object或指定的上限类型)接收,丢失了具体的类型信息,需要手动进行类型转换,这增加了编程的复杂度和潜在的风险。
总结:
<? extends T>
主要用于读取操作,保证了消费者(consumer)的安全性,但限制了生产者(producer)的能力。<? super T>
则适用于插入操作,作为生产者的角色很合适,但在消费数据时丧失了泛型的优势,需要额外的类型转换。
理解这两者的区别和限制对于正确使用泛型和避免编译时错误至关重要。
5.在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常。
在Java中,如果将一个无泛型限制定义的集合(即原始类型集合,如List
、Map
等,没有具体指定泛型参数)赋值给一个有泛型限制的集合变量,编译器会发出警告,因为这样会绕过泛型的类型安全检查。这样做虽然在编译时可以通过,但是在运行时可能会遇到类型转换异常(ClassCastException
),尤其是在从集合中获取元素并强制类型转换时。
示例:
1List rawList = new ArrayList(); // 无泛型的原始类型集合
2rawList.add("Hello"); // 向集合中添加字符串
3rawList.add(123); // 同时也可以添加整数,这是不安全的
4
5List<String> stringList = (List<String>) rawList; // 转型警告,但可以编译通过
6
7String s = stringList.get(0); // 正常,得到"Hello"
8String s2 = stringList.get(1); // 运行时错误,尝试将Integer转换为String,抛出ClassCastException
为了避免在使用这样的集合时抛出ClassCastException
,通常需要在获取元素前进行instanceof
判断,确认元素的实际类型与预期相符,然后再进行类型转换。
改进后的安全访问方式:
1for (Object obj : stringList) {
2 if (obj instanceof String) {
3 String str = (String) obj;
4 System.out.println(str);
5 } else {
6 System.out.println("发现非预期类型: " + obj.getClass().getName());
7 }
8}
在这个例子中,通过instanceof
检查确保了只有当元素确实是预期的类型(在这里是String
)时,才进行类型转换并使用。这样做虽然牺牲了一定的简洁性和泛型的优势,但增强了程序的健壮性,避免了运行时的类型转换异常。
最佳实践:
尽量避免使用原始类型集合,始终使用带有具体泛型参数的集合定义,这样可以充分利用Java的泛型类型检查机制,从源头上减少类型不匹配的问题。如果必须处理来自外部的原始类型集合,使用泛型限定的临时集合进行转换,并在必要时进行类型检查,是较为安全的做法。
6.在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常。
从JDK 7开始,为了提高排序算法的性能并避免潜在的并发问题,Arrays.sort
和Collections.sort
方法要求自定义的Comparator
实现需满足以下三个条件,否则会在排序时抛出IllegalArgumentException
异常:
- 自反性:对于所有x,
compare(x, x)
必须返回0。 - 对称性:对于所有x和y,
compare(x, y) == -compare(y, x)
。 - 传递性:对于所有x、y、z,如果
compare(x, y) < 0
且compare(y, z) < 0
,那么compare(x, z) < 0
。
违反这些条件的Comparator
可能导致排序算法进入无限循环或产生未定义的结果。
例子说明:
下面是一个不满足上述条件的Comparator
示例,以及它如何导致IllegalArgumentException
:
1import java.util.Arrays;
2import java.util.Comparator;
3
4public class ComparatorExample {
5
6 public static void main(String[] args) {
7 // 定义一个不满足对称性的Comparator
8 Comparator<String> problematicComparator = new Comparator<String>() {
9 @Override
10 public int compare(String o1, String o2) {
11 // 这个Comparator只根据字符串长度比较,但没有正确处理对称性
12 return o1.length() - o2.length();
13 }
14 };
15
16 try {
17 // 尝试使用该Comparator进行排序
18 String[] array = {"apple", "banana", "pear"};
19 Arrays.sort(array, problematicComparator); // 这里可能抛出IllegalArgumentException
20 } catch (IllegalArgumentException e) {
21 System.err.println("排序失败,因为Comparator不满足排序所需的条件。");
22 e.printStackTrace();
23 }
24 }
25}
尽管在这个特定情况下JDK可能不会直接抛出异常(因为它主要检查可能导致无限循环的情况),但这个示例展示了如何编写一个不符合排序要求的Comparator,从而可能导致不可预测的行为或错误。
正确的做法是确保你的Comparator
实现满足所有三个条件,以保证排序的正确性和效率。
让我们修改上面的例子,确保自定义的Comparator
实现满足自反性、对称性和传递性这三个条件。这里,我将基于字符串的自然顺序(字典顺序)和长度来进行综合比较,同时确保遵循所有排序规则:
1import java.util.Arrays;
2import java.util.Comparator;
3
4public class ComparatorExample {
5
6 public static void main(String[] args) {
7 // 定义一个满足所有条件的Comparator
8 Comparator<String> improvedComparator = new Comparator<String>() {
9 @Override
10 public int compare(String o1, String o2) {
11 // 首先按照字符串长度进行比较
12 int lengthCompare = Integer.compare(o1.length(), o2.length());
13 // 如果长度相同,则按照字典顺序进行比较
14 if (lengthCompare == 0) {
15 return o1.compareTo(o2);
16 }
17 return lengthCompare;
18 }
19 };
20
21 // 使用该Comparator进行排序
22 String[] array = {"apple", "banana", "pear", "cherry"};
23 Arrays.sort(array, improvedComparator);
24
25 // 打印排序后的数组
26 System.out.println(Arrays.toString(array));
27 }
28}
在这个修改后的例子中,improvedComparator
首先比较字符串的长度,如果长度相同,则进一步按照字典顺序进行比较。这种实现方式确保了:
- 自反性:
compare(x, x)
总是返回0,因为两个相同对象的长度相等,且字典顺序比较也必然相等。 - 对称性:如果
compare(x, y)
返回一个非零值,那么compare(y, x)
会返回相反的值,因为不管是长度比较还是字典顺序比较都满足对称性。 - 传递性:由于长度比较和字典顺序比较都是传递的,组合后的比较结果也是传递的。
这样,我们得到了一个既符合排序逻辑,又满足所有排序条件的Comparator
实现。