选择语言
durumis AI 总结的文章
- 使用 new 關鍵字建立字串或 Boolean 物件會浪費記憶體,因此最好使用字面值宣告,或使用 Boolean.valueOf() 方法。
- String.matches() 方法會使用正規表示式,因此可能會導致效能問題,最好快取 Pattern 物件並重複使用。
- 對於像 keySet() 方法一樣返回檢視物件的情況,使用防禦性複製返回新物件會更安全。
在创建不必要对象的情况下
使用 new String()
String a = new String("hi");
String b = new String("hi");
字符串 a、b、c 都具有“hi”字符串。但是,由于这三个字符串引用的地址都不同,因此会导致为相同数据分配不同内存的浪费。
因此,在声明字符串时,应使用字面量,而不是 new 关键字。
String a = "hi";
String b = "hi";
以上代码只使用一个实例。此外,使用这种方法可以确保在同一个 JVM 中使用“hi”字符串字面量的所有代码都重用同一个对象。这是由于 Java 常量池的特性。
使用 new Boolean()
以上代码通过接收字符串作为参数的构造函数来创建 Boolean 实例。Boolean 只有 true 或 false,每次创建实例都会浪费内存。因此,最好使用静态工厂方法 Boolean.valueOf()。
使用 String.matches()
如果创建成本很高,最好缓存并重用它,但我们并不总是能够知道我们创建的对象的成本。例如,如果我们想编写一个方法来检查给定的字符串是否为有效的罗马数字,我们可以使用正则表达式,如下所示。
public static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
但是 String.matches() 方法在性能方面存在问题。该方法在内部创建的用于正则表达式的 Pattern 实例在使用一次后就被丢弃,并立即成为垃圾回收的目标。如果该正则表达式被重复使用的频率很高,则创建和丢弃相同的 Pattern 实例的成本就会很高。因此,最好预先缓存 Pattern 实例,并在以后每次调用 isRomanNumeral() 方法时重用它。
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
public static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
注意
在所有以上示例中,在缓存不必要对象时,我们都将其创建为不可变对象。这是因为这样做才能确保安全地重用它们。但是,在某些情况下,这与重用的直觉相矛盾。
适配器(视图)是一种将实际操作委托给后端对象,而自身充当第二个接口的对象。适配器只需要管理后端对象,因此对于每个后端对象,我们只需要创建一个适配器。
例如,Map 接口的 keySet() 方法返回包含 Map 对象中所有键的 Set 视图。用户可能会认为每次调用 keySet() 方法时都会创建一个新的 Set 实例,但实际上,查看 JDK 实现的内容,会发现它始终返回同一个可变 Set 实例。
这是因为即使返回的 Set 实例是可变的,它执行的功能也都是相同的,并且所有 Set 实例都代表 Map 实例。因此,即使 keySet() 创建多个视图对象,也无关紧要,也没有必要这样做,也没有任何好处。
public class UsingKeySet {
public static void main(String[] args) {
Map menu = new HashMap<>();
menu.put("Burger", 8);
menu.put("Pizza", 9);
Set names1 = menu.keySet();
Set names2 = menu.keySet();
names1.remove("Burger");
System.out.println(names1.size()); // 1
System.out.println(names2.size()); // 1
}
因此,如果修改 names1 实例,names2 实例也会受到影响。
但是,我个人认为 keySet() 方法的返回值应该使用防御性复制,每次都返回一个新对象。如果从 keySet() 方法接收到的 Set 实例在其他地方也使用,并且有代码更改该实例的状态,那么我们无法确定当前使用的 Set 实例和 Map 实例的值。
此外,除非过度使用 keySet(),否则 Set 接口的每次创建都不会对性能产生致命影响。我认为最好将 Set 接口创建为不可变对象,以便稳定地维护和修复它。
自动装箱
自动装箱是指在程序员混合使用基本类型和包装类型时,自动进行相互转换的技术。但是,自动装箱只会模糊基本类型和包装类型之间的界限,并不能完全消除它们。
public static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
从逻辑上讲,没有问题,但在性能上非常低效。其原因在于 sum 的类型和 for 循环中的 i 的类型。
sum 的类型为 Long 类型,i 为 long 类型。这意味着当 long 类型的 i 在循环中不断增加并添加到 sum 中时,每次都会创建一个新的 Long 实例。因此,最好使用基本类型而不是包装类型,并注意避免使用意外的自动装箱。
不要误解
不要误解避免创建不必要对象的含义,即由于创建对象的成本很高,因此应该避免创建它们。
特别是现在 JVM 创建和回收不必要的小对象的成本并不高。因此,除非是数据库连接等成本非常高的对象,否则不要创建自定义对象池。
此外,请记住,在需要防御性复制的情况下重用对象所带来的损失,远大于不必要地重复创建对象所带来的损失。重复创建对象的副作用只会影响代码形式和性能,而防御性复制失败会导致错误和安全问题。