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

相关推荐
2401_857297911 小时前
秋招内推--招联金融2025
java·前端·算法·金融·求职招聘
小帅吖1 小时前
浅析Golang的Context
开发语言·后端·golang
-$_$-2 小时前
【黑马点评】2 商户查询缓存
java·jmeter·缓存·maven
2401_857439692 小时前
春潮涌动:构建“衣依”服装销售平台的Spring Boot之旅
java·spring boot·后端
2401_854391082 小时前
Spring Boot与足球青训后台系统的协同
java·spring boot·后端
杨哥带你写代码3 小时前
美容院管理创新:SpringBoot系统设计与开发
java·spring boot·后端
九离⁢3 小时前
SpirngBoot核心思想之一IOC
java·spring boot
鸽鸽程序猿3 小时前
【JavaSE】反射、枚举、lambda表达式
java
jast_zsh3 小时前
详细介绍:API 和 SPI 的区别
java
学编程的小鬼3 小时前
Object类
java