Effective Java 简要笔记

创建和销毁对象

用静态工厂方法替代构造器

  • 静态工厂方法具有名称,更具有可读性,尤其是构造器比较多的类;
  • 通过静态工厂方法创建对象时,可以不必每次创建对象,使用预先构建好的实例,例如Boolean类:
typescript 复制代码
public static Boolean valueOf(boolean b) {  
return (b ? TRUE : FALSE);  
}
  • 工厂方法可以返回原类型的任何子类,这相比构造器更加灵活。

一些静态方法的惯用名称:

  • from, 类型转换方法,只有一个参数,返回该类型对应的实例。
ini 复制代码
Date d = Date.from(instance);
  • of, 聚合方法,多个参数,返回一个实例。
ini 复制代码
Set<Rank> faceCards = EnumSet.of(JACK, QUEUE);

遇到多个构造器参数时考虑使用构建器

静态工厂方法和构造器都不能很好的扩展到大量参数。对于大量参数的场景,一种方法是使用重叠构造器的方式,第一个构造器只有少量必要参数,第二个构造器除了必要参数,还添加一些可选参数,以此类推。重叠构造器在参数量可控的时候还好,随着参数增多,构造器方法也会爆炸式增多,变得难以维护。另一种办法是Java Beans模式,通过无参构造器构造对象,调用setter方法传递参数,但是这种方法会导致对象在构建过程中处于不一致状态,而且把类做成不可变的可能不复存在。

构建器模式通过让调用方传递必要参数调用Builder类的构造器得到builder对象,然后调用可选参数的setter方法设置可选参数,最后调用build方法生成不可变的最终对象。

kotlin 复制代码
public final class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;

    // 私有构造函数,仅通过Builder类实例化
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
    }

    // Getter方法
    public int getServingSize() {
        return servingSize;
    }

    public int getServings() {
        return servings;
    }

    public int getCalories() {
        return calories;
    }

    // 构建器类
    public static class Builder {
        // 必填属性
        private final int servingSize;
        private final int servings;

        // 可选属性
        private int calories = 0;
 

        // 构造函数,设置必填属性
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        // 设置可选属性的方法
        public Builder calories(int val) {
            this.calories = val;
            return this;
        }

        // 构建最终的NutritionFacts对象
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

所有的字段都是 final 的,并且没有提供任何 setter 方法,对象一旦创建,其状态就不能再改变。 Builder 类用于构建 NutritionFacts 对象,Builder 类的构造函数接受必填属性。,Builder 类提供了链式调用的方法来设置可选属性。 最后,通过 build 方法创建 NutritionFacts 对象。

用私有构造器或者枚举类型强化Singleton属性

Singleton是指仅仅需要被实例化一次的对象,通常用来代表没有状态的对象。通过定义私有构造器,提供公有静态域或者工厂方法来获取单例对象。私有构造方法可以保证调用方无法通过构造器获取对象,只能获取创建好的单例对象,为防止通过反射机制调用私有构造器,可以在第二次创建对象时抛出异常。另一种实现单例的方法是声明一个包含单个元素的枚举类型,这种方法提供了序列化机制,我们不用考虑单例对象在序列化和反序列化时需要做的额外工作。

arduino 复制代码
public enum Singleton {
    INSTANCE;
}

通过私有构造器强化不可实例的能力

一些工具类只是用来提供一些静态变量或者静态的工具方法,不希望被实例化,因为实例化没有意义。但是在缺少显式声明的构造器时,编译器会自动提供一个无参的构造器,还是能被实例化和继承。正确的做法是提供一个私有的构造器,让这种类不能被继承,也不能被实例化。

arduino 复制代码
public final class Arrays {  
    private Arrays() {}
}
csharp 复制代码
public class Collections {  
    private Collections() {}
}

避免创建不必要的对象

下面这段代码,将变量sum声明为Long类型以后,每次在做加法操作时,会先将int类型的i转变为Long类型的实例,这将导致构建大量的Long实例。要优先使用基本类型而不是对应的装箱类,防止无意识的自动装箱。

ini 复制代码
private static long sun() {  
    Long sum = 0L;  
    for (int i = 0; i < Integer.MAX_VALUE; i++) {  
        sum += i;  
    }  
    return sum;  
} 

try-with-resources 优先于try-finally

使用try-finally来关闭资源存在一些问题,比如在try块和finally块中都抛出异常,try块中抛出的异常会被finally块中抛出的异常完全抹除,在异常堆栈轨迹中完全没有try块中的异常记录。使用try-with-resources可以解决这个问题。资源必须实现 java.lang.AutoCloseable 接口,这样才能在 try-with-resources 语句中使用,在 try 块结束时,所有声明的资源都会自动关闭。

typescript 复制代码
public class TryWithResourcesExample {
    public static void main(String[] args) {
        // 使用 try-with-resources 语句
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在 try 语句的括号内声明资源,如果是多个资源,使用分号分割。在 try 块中使用声明的资源,如果在 try 块中或资源关闭时发生 IOException,会捕获并处理异常。如果处理资源或者关闭资源都发生了异常,后一个异常会被禁止,保留第一个异常,禁止的异常会被打印到堆栈轨迹中,也可以通过java.lang.Throwable#getSuppressed获取。

对于所有对象都通用的方法

覆盖equals方法时请遵守通用约定

如果类具有自己逻辑相等的概念,应该覆盖equals方法。覆盖时,遵守以下约定:

  • 自反性,对于非空性x,x.equals(x)必须返回true;
  • 对称性,对于非空引用x, y, x.equals(y)为true时,y.equals(x)也必须为true;
  • 传递性,对于非空引用x, y, z, x.equals(y)为true,y.equals(z)为true,x.equals(z)也必须为true。
  • 一致性,对于非空引用x, y, x.equals(y)的多次调用结果应该相同;
  • 非空性,非空引用x,x.equals(null)必须为false。

实现高质量equals方法的诀窍:

  1. 使用==操作符检查参数是否为当前对象的引用,如果是,则返回true;
  2. 使用instanceof操作符检查参数类型是否为当前类型,不是则返回false;
  3. 把参数转换为当前类型;
  4. 根据逻辑相等的定义,对关键域进行相等判断,如果全部判断相等,则返回true,否则返回false。对于既不是float也不是double的基本类型,使用==操作符判断;对于float类型或者double类型,使用java.lang.Float#compare(float f1, float f2)或者java.lang.Double#compare(double d1, double d2)进行判断;对于引用类型,可以递归调用引用类型的equals方法。

equals方法不需要对入参进行null检查,因为类型检查会返回false。

覆盖equals方法时总要覆盖hashCode方法

这是因为如果两个对象调用equals方法比较是相等的,则hashCode方法返回值也必须一致,否则该类无法结合所有基于散列的集合一起工作。比如两个通过equal方法判定为相等的对象,在添加到HashSet中时,由于没有覆盖hashCode方法,会重复添加,但是根据Set的定义,Set中的元素应该是唯一的,不能有两个"相等"的对象。

始终覆盖toString

typescript 复制代码
public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}

这是默认的toString实现,通常情况下,需要和对象相关的更独特的信息,而不是类名和散列值。

考虑实现Comparable接口

类一旦实现了Comparable接口,就可以跟许多范型算法以及依赖于该接口的集合实现进行协作。例如java.util.Collections接口中的方法:

typescript 复制代码
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}

以及java.time.Year:

arduino 复制代码
public int compareTo(Year other) {  
    return year - other.year;  
}

Comparable接口只有一个compare方法,返回结果小于0表示当前值小于参数值,等于0表示相等,大于0表示当前值大于参数值。 对于基本类型,所对应的装箱类型都已经提供了compara方法,也不必在使用关系操作符<和>进行比较。

使类和成员的可访问性最小

设计良好的组件会隐藏所有的实现细节,这可以有效的解除组件之间的耦合关系,使得这些组件可以独立的开发、测试和修改。对于顶层的类和接口,只有两种访问级别,包级私有和公有的。类或者接口使用pulic修饰就是公有的,否则是包级私有的。如果一个包级私有的类只有使用它的类用到,就应该考虑将这个类设计为私有内部类。

对于成员,有四种访问级别,私有的、包级私有、受保护的、公开的。公有类的实例域决不能被公开,如果实例域是公开的并且是非final的,公开之后,就等于放弃了存储在这个域值值的控制能力。对于公开的final的数组域,或者提供了返回域的方法也是错误的,会导致数组内容被修改,如果必有,应该将数组域设置为私有,只提供一个数组拷贝。

使可变性最小化

不可变类是指其实例不能被修改的类。每个实例包含的信息都应该在创建该实例时提供,并且在对象的整个生命周期内不可变。不可变类更加易于设计、实现和使用,而且不易出错。 设计不可变类遵循以下原则:

  1. 不要为类提供setter方法;
  2. 保证类不会被继承,通过final修饰类或者构造方法私有的方式;
  3. 所有域都是private final修饰的,在构造时就需要赋值,并且不允许修改;
  4. 如果类具有指向可变对象的域,需要确保使用该类对象的客户端无法获得指向可变对象的引用。

接口优先于抽象类

对于在设计抽象类时,应该首先考虑一下,这个抽象类能不能设计成为接口,相比于抽象类,现有的实现了接口的类更容易被更新,因为Java语言允许实现多个接口,但是只允许继承一个抽象类。

接口虽然可以提供缺省方法,为某些方法提供实现,但是缺省方法仍然有一些缺点:接口无法给equals、hashCode等方法提供缺省实现;接口中不能包含非公有的静态域或者实例域。 为了结合接口和抽象类的优势,通过对接口提供一个抽象的骨架实现,接口负责定义类型,或者提供一些缺省方法,而骨架实现类则负责提供除基本方法之外的方法。

常量接口是对接口的不良使用

类在内部使用某些常量,属于实现细节,实现常量接口会导致把这样的实现细节泄露到该类导出的API中。以java.io.ObjectStreamConstants为例:

java 复制代码
public interface ObjectStreamConstants {  
    static final short STREAM_MAGIC = (short)0xaced;  

    static final short STREAM_VERSION = 5;  
 
    static final byte TC_BASE = 0x70;
    // 审略部分代码
}

如果要导出常量,首先考虑这些常量是不是与某个类或者接口紧密相关,如果是,应该把常量添加在这些接口或者类中;其次,使用枚举类型导出这些常量;最后,考虑使用不可实例化的工具类进行导出。

静态成员类优先于非静态成员类

如果成员类没有访问外围类实例的需求,就应该把成员类设计为static的,因为非static的成员类需要外围实例才能创建,在创建成员类实例以后,成员类实例会持有外围类实例的引用,保留这份引用不但需要消耗空间,而且会导致外围类实例不能被GC,造成内存泄漏。

范型

不要使用原生态类型

如果使用原生类型,会失去范型在安全性和描述性方面的所有优势,下面的List在声明时没有指定类型,所以可以加入任何类型的元素,但是在从list中获取元素时很可能强制转换失败,除非使用Object类型。

csharp 复制代码
public List test() {  
    List list = new ArrayList();  
    list.add(1);  
    list.add("zz");  
    return list;  
}

优先考虑范型

使用泛型类时,编译器会在编译阶段检查类型安全,避免运行时出现类型转换错误。在使用非泛型类时,通常需要进行强制类型转换,这可能导致 ClassCastException 异常。使用泛型类后,编译器会自动处理类型转换,提高代码的安全性。泛型类可以处理多种数据类型,使得类的设计更加通用,适用于不同的场景。

typescript 复制代码
public class Main {
    public static void main(String[] args) {
        Box box = new Box();
        box.set("Hello");
        String message = (String) box.get(); // 需要强制类型转换
        System.out.println(message);
    }
}

优先考虑范型方法

静态工具方法与范型类一样,尤其适合范型化。声明范型方法时,声明类型参数化的类型参数列表,处在方法的修饰符和返回值之间。

c 复制代码
   // 定义一个泛型方法,用于打印任何类型的数组
    public <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

利用有限制通配符提高API灵活性

两个通配符:? extends T表示它可以提供 T 或其子类型的数据,? super T,表示它可以接收 T 或其父类型的数据。根据PECS原则使用。Producer Extends:当类型参数用于从集合中获取数据时,使用 ? extends T。 Consumer Super:当类型参数用于向集合中添加数据时,使用 ? super T。一言以蔽之,生产者可以把范围缩小(衍生到子类),消费者需要把支持范围扩大。

typescript 复制代码
import java.util.ArrayList;
import java.util.List;

public class PECSExample {

    // 生产者使用 ? extends
    public void printNumbers(List<? extends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }

    // 消费者使用 ? super
    public void addNumbers(List<? super Integer> numbers, int... values) {
        for (int value : values) {
            numbers.add(value);
        }
    }

    public static void main(String[] args) {
        PECSExample example = new PECSExample();

        // 生产者示例
        List<Integer> integers = new ArrayList<>();
        integers.add(1);
        integers.add(2);
        example.printNumbers(integers); // 输出: 1, 2

        List<Number> numbers = new ArrayList<>();
        numbers.add(3.14);
        numbers.add(42);
        example.printNumbers(numbers); // 输出: 3.14, 42

        // 消费者示例
        List<Integer> integerList = new ArrayList<>();
        example.addNumbers(integerList, 1, 2, 3);
        System.out.println(integerList); // 输出: [1, 2, 3]

        List<Number> numberList = new ArrayList<>();
        example.addNumbers(numberList, 4, 5, 6);
        System.out.println(numberList); // 输出: [4, 5, 6]
    }
}

枚举和注解

用enum代替int常量

枚举相比int常量有很多优势,第一是枚举类型保证类编译时安全,其次,枚举类型还能添加任意的域和方法并实现接口。枚举类具有更多的描述信息,其每个实例的toString方法会返回枚举值声明的名称,更有描述意义。

使用EnumSet代替位域

EnumSet 是 Java 集合框架中专门为枚举类型设计的一种高效集合。它提供了许多优点,使其成为处理枚举类型集合的首选工具。EnumSet 的实现非常高效,因为它利用了位向量(bit vector)来存储枚举值,因此占用的内存非常少。EnumSet 是类型安全的,只能包含特定枚举类型的元素。

EnumSet 提供了多种工厂方法来创建集合,常用的有: noneOf:创建一个空的 EnumSet。 of:创建一个包含指定元素的 EnumSet。 copyOf:创建一个包含另一个集合所有元素的 EnumSet。 range:创建一个包含指定范围内的所有枚举值的 EnumSet。

csharp 复制代码
public enum Color {
    RED, GREEN, BLUE, YELLOW, PURPLE
}

public class EnumSetExample {

    public static void main(String[] args) {
        // 创建一个空的 EnumSet
        EnumSet<Color> emptySet = EnumSet.noneOf(Color.class);
        System.out.println("Empty Set: " + emptySet);

        // 创建一个包含指定元素的 EnumSet
        EnumSet<Color> someColors = EnumSet.of(Color.RED, Color.GREEN);
        System.out.println("Some Colors: " + someColors);

        // 创建一个包含另一个集合所有元素的 EnumSet
        Set<Color> anotherSet = new HashSet<>(Arrays.asList(Color.BLUE, Color.YELLOW));
        EnumSet<Color> copiedSet = EnumSet.copyOf(anotherSet);
        System.out.println("Copied Set: " + copiedSet);

        // 创建一个包含指定范围内的所有枚举值的 EnumSet
        EnumSet<Color> rangeSet = EnumSet.range(Color.GREEN, Color.PURPLE);
        System.out.println("Range Set: " + rangeSet);

        // 添加和删除元素
        someColors.add(Color.BLUE);
        someColors.remove(Color.RED);
        System.out.println("Modified Set: " + someColors);

        // 检查元素是否存在
        boolean containsGreen = someColors.contains(Color.GREEN);
        System.out.println("Contains Green: " + containsGreen);

        // 遍历集合
        for (Color color : someColors) {
            System.out.println(color);
        }
    }
}

用EnumMap代替序数索引

EnumMap 内部使用一个数组来存储键值对,数组的索引对应枚举常量的序号(ordinal)。 枚举常量的序号是从 0 开始的,因此 EnumMap 可以通过枚举常量的序号快速定位到对应的值。相比序数索引,EnumMap 是类型安全的,只能接受特定枚举类型的键。由于 EnumMap 内部使用数组存储键值对,因此查找、插入和删除操作的时间复杂度都是 O(1)。

csharp 复制代码
public enum DayOfWeek {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class EnumMapExample {

    public static void main(String[] args) {
        // 创建一个空的 EnumMap
        EnumMap<DayOfWeek, String> emptyMap = new EnumMap<>(DayOfWeek.class);
        System.out.println("Empty Map: " + emptyMap);

        // 创建一个包含初始键值对的 EnumMap
        EnumMap<DayOfWeek, String> dayMap = new EnumMap<>(DayOfWeek.class);
        dayMap.put(DayOfWeek.MONDAY, "星期一");
        dayMap.put(DayOfWeek.TUESDAY, "星期二");
        dayMap.put(DayOfWeek.WEDNESDAY, "星期三");
        System.out.println("Initial Map: " + dayMap);

        // 添加和删除键值对
        dayMap.put(DayOfWeek.THURSDAY, "星期四");
        dayMap.remove(DayOfWeek.MONDAY);
        System.out.println("Modified Map: " + dayMap);

        // 获取值
        String value = dayMap.get(DayOfWeek.TUESDAY);
        System.out.println("Value for TUESDAY: " + value);

        // 检查键是否存在
        boolean containsKey = dayMap.containsKey(DayOfWeek.FRIDAY);
        System.out.println("Contains FRIDAY: " + containsKey);

        // 遍历映射
        for (Map.Entry<DayOfWeek, String> entry : dayMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 创建一个包含另一个映射所有键值对的 EnumMap
        Map<DayOfWeek, String> anotherMap = new HashMap<>();
        anotherMap.put(DayOfWeek.FRIDAY, "星期五");
        anotherMap.put(DayOfWeek.SATURDAY, "星期六");
        EnumMap<DayOfWeek, String> copiedMap = new EnumMap<>(anotherMap);
        System.out.println("Copied Map: " + copiedMap);
    }
}

Lambda 和Stream

Lambda优先于匿名类

对于函数接口,建议使用Lambda。Lambda表达式支持类型推断,减少了显式类型声明的需要,使代码更加简洁和易读。JVM对Lambda表达式进行了优化,提高了运行时性能。Lambda表达式支持函数式编程风格,便于进行流式操作,通过使用Lambda表达式,可以显著提升代码的质量和开发效率。

但是,Lambda没有名称和文档,如果一个计算本身不是自描述的,或者超出了几行,就不要放在一个lambda里面。在lambda表达式中的this表示外围实例,在匿名类中this表示匿名类实例,这里需要留意。

方法引用优先于Lambda

方法引用比lambda更简洁,如果lambda过于复杂,可以从lambda提取代码,放到新的方法中,使用该方法的一个引用代替lambda。使用方法引用需要先了解函数式接口:

  • Conmsumer,接受一个参数,不返回结果
arduino 复制代码
     import java.util.function.Consumer;

     public class ConsumerExample {
         public static void main(String[] args) {
             Consumer<String> consumer = System.out::println;
             consumer.accept("Hello, World!");
         }
     }
     
  • Function<T, R>,接受一个参数,返回一个结果。
typescript 复制代码
     import java.util.function.Function;

     public class FunctionExample {
         public static void main(String[] args) {
             Function<String, Integer> function = Integer::parseInt;
             int result = function.apply("123");
             System.out.println(result); // 输出 123
         }
     }
     
  • Predicate,接受一个参数,返回一个布尔值。
typescript 复制代码
     import java.util.function.Predicate;

     public class PredicateExample {
         public static void main(String[] args) {
             Predicate<String> predicate = String::isEmpty;
             boolean result = predicate.test("");
             System.out.println(result); // 输出 true
         }
     }
     
  • Supplier,不接受参数,返回一个结果
java 复制代码
import java.util.function.Supplier;

     public class SupplierExample {
         public static void main(String[] args) {
             Supplier<Long> supplier = System::currentTimeMillis;
             long result = supplier.get();
             System.out.println(result); // 输出当前时间的毫秒数
         }
     }
     
  • UnaryOperator,接受一个参数,返回相同类型的结果。
arduino 复制代码
    import java.util.function.UnaryOperator;

     public class UnaryOperatorExample {
         public static void main(String[] args) {
             UnaryOperator<String> operator = String::toUpperCase;
             String result = operator.apply("hello");
             System.out.println(result); // 输出 HELLO
         }
     }
     
  • BiConsumer<T, U>,接受两个参数,不返回结果。
arduino 复制代码
     import java.util.function.BiConsumer;

     public class BiConsumerExample {
         public static void main(String[] args) {
             BiConsumer<String, String> biConsumer = System.out::println;
             biConsumer.accept("Hello, ", "World!");
         }
     }
     
  • BiFunction<T, U, R>,接受两个参数,返回一个结果。
arduino 复制代码
     import java.util.function.BiFunction;

     public class BiFunctionExample {
         public static void main(String[] args) {
             BiFunction<Integer, Integer, Integer> biFunction = Integer::sum;
             int result = biFunction.apply(3, 5);
             System.out.println(result); // 输出 8
         }
     }
     
  • 自定义函数接口
arduino 复制代码
    @FunctionalInterface
public interface MyFunction<T, R> {
    R apply(T t);
}

public class CustomFunctionExample {
    public static void main(String[] args) {
        MyFunction<String, Integer> myFunction = Integer::parseInt;
        int result = myFunction.apply("123");
        System.out.println(result); // 输出 123
    }
}

方法

检查参数有效性

应该在执行方法之前对参数进行检查,如果传递了无效参数,应该尽快·出现合适的异常进行提示。对于公有或者受保护的方法,要用Javadoc@throws标签在文档中说明违反参数限制时会跑出的异常。 经常需要对空指针进行检查,可以使用java.util.Objects#requireNonNull(T, java.lang.String):

typescript 复制代码
public static <T> T requireNonNull(T obj, String message) {  
    if (obj == null)  
        throw new NullPointerException(message);  
    return obj;  
}

对于越界检查,也有相应的工具方法:java.util.Objects#checkFromToIndex(int, int, int),java.util.Objects#checkIndex(int, int)。

慎用重载

对象的运行时类型并不影响哪个重载版本将被执行,选择工作在编译时就已经确定,这不同于重写,当调用被覆盖的方法时,最具体的子类的方法将被调用,而不是其父类的方法。

应该避免使用具有相同参数数目的重载方法,可以通过修改方法名称的方式实现这一点。

慎用可变参数

每次调用可变参数方法,都涉及到一次数组的分配和初始化。因为可变参数的机制首先会创建一个数组,数组的大小为调用时传递的参数的数量,然后将参数传值传入数组,再将数组传递给方法。

arduino 复制代码
    public class VarargsExample {
    public static void main(String[] args) {
        int sum = sumIntegers(1, 2, 3, 4, 5);
        System.out.println("Sum: " + sum);
    }

    public static int sumIntegers(int... numbers) {
        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        return sum;
    }
}

在重视性能的情况下,如果无法承受创建数组的成本,可以通过重载方法的方式避免使用可变参数,比如90%的情况参数不会超过5个,那就定义五个重载方法,超过5个参数的再使用可变参数。

csharp 复制代码
    public void foo() {  
}  
  
public void foo(int a1) {  
}  
  
// 省略  
public void foo(int a1, int a2, int a3, int a4, int a5) {  
}  
  
public void foo(int a1, int a2, int a3, int a4, int a5, int... restArgs) {  
}

返回0长度的数组或者集合,而不是null

bad case:

csharp 复制代码
private final List<Integer> codes =...;  
  
public List<Integer> getCodes() {  
    return codes.isEmpty() ? null : new ArrayList<>(codes);  
}

调用方需要专门处理返回null的情况,这容易导致出错。 对于返回没有包含元素的list的情况,可以使用java.util.Collections#emptyList,对于返回set的情况,可以使用java.util.Collections#emptySet,对于返回map的情况,可以使用java.util.Collections#emptyMap。

异常

受检异常与运行时异常

受检异常是指那些在编译时必须处理的异常。也就是说,如果一个方法可能会抛出受检异常,那么调用该方法的代码必须显式地处理这个异常,要么通过 try-catch 块捕获,要么通过 throws 关键字声明抛出。非受检异常是指那些在编译时不需要强制处理的异常。这类异常通常是由于程序逻辑错误引起的,如空指针异常、数组越界等。非受检异常继承自 RuntimeException 或其子类。

优先使用标准异常

  • IllegalArgumentException:当传递给方法的参数不合适时抛出。

  • IllegalStateException:当调用方法的对象处于不允许该方法调用的状态时抛出。

  • NullPointerException:当尝试访问空对象的成员时抛出。

  • UnsupportedOperationException:当不支持某个操作时抛出。

  • IndexOutOfBoundsException:当索引超出范围时抛出。

  • ConcurrentModificationException:当检测到并发修改时抛出。

不要忽略异常

csharp 复制代码
public class IgnoreExceptionExample {
    public static void readFile() {
        try {
            FileReader reader = new FileReader("file.txt");
            // 其他读取文件的操作
            reader.close();
        } catch (IOException e) {
            // 忽略异常
           
        }
    }
}

空的catch块会使异常达不到应有的目的,抛出了异常,但是异常没有被处理或者传播出去,导致问题被隐藏,不能及时暴露,会给排查造成阻碍。如果确定不需要处理,最好加一条注释。

相关推荐
普通网友10 分钟前
KUD#73019
java·php·程序优化
IT_陈寒13 分钟前
Redis 性能翻倍的 5 个隐藏技巧,99% 的开发者都不知道第3点!
前端·人工智能·后端
JaguarJack14 分钟前
PHP 桌面端框架NativePHP for Desktop v2 发布!
后端·php·laravel
番茄Salad14 分钟前
自定义Spring Boot Starter项目并且在其他项目中通过pom引入使用
java·spring boot
程序员三明治26 分钟前
详解Redis锁误删、原子性难题及Redisson加锁底层原理、WatchDog续约机制
java·数据库·redis·分布式锁·redisson·watchdog·看门狗
自由的疯36 分钟前
Java 怎么学习Kubernetes
java·后端·架构
自由的疯36 分钟前
Java kubernetes
java·后端·架构
普通网友2 小时前
IZT#73193
java·php·程序优化
rechol2 小时前
C++ 继承笔记
java·c++·笔记
Han.miracle5 小时前
数据结构——二叉树的从前序与中序遍历序列构造二叉树
java·数据结构·学习·算法·leetcode