• 作者:老汪软件技巧
  • 发表时间:2024-10-05 04:00
  • 浏览量:

创建和销毁对象用静态工厂方法替代构造器

public static Boolean valueOf(boolean b) {  
return (b ? TRUE : FALSE);  
}

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

Date d = Date.from(instance);

Set faceCards = EnumSet.of(JACK, QUEUE);

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

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

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

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

public enum Singleton {
    INSTANCE;
}

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

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

public final class Arrays {  
    private Arrays() {}
}

public class Collections {  
    private Collections() {}
}

避免创建不必要的对象

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

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 块结束时,所有声明的资源都会自动关闭。

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方法。覆盖时,遵守以下约定:

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

使用==操作符检查参数是否为当前对象的引用,如果是,则返回true;使用instanceof操作符检查参数类型是否为当前类型,不是则返回false;把参数转换为当前类型;根据逻辑相等的定义,对关键域进行相等判断,如果全部判断相等,则返回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

public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}

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

考虑实现Comparable接口

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

public static extends Comparablesuper T>> void sort(List list) {
    list.sort(null);
}

以及java.time.Year:

public int compareTo(Year other) {  
    return year - other.year;  
}

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

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

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

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

使可变性最小化

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

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

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

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

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

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

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,造成内存泄漏。

Effective Java 简要笔记_Effective Java 简要笔记_

范型不要使用原生态类型

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

public List test() {  
    List list = new ArrayList();  
    list.add(1);  
    list.add("zz");  
    return list;  
}

优先考虑范型

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

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);
    }
}

优先考虑范型方法

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

   // 定义一个泛型方法,用于打印任何类型的数组
    public  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。一言以蔽之,生产者可以把范围缩小(衍生到子类),消费者需要把支持范围扩大。

import java.util.ArrayList;
import java.util.List;
public class PECSExample {
    // 生产者使用 ? extends
    public void printNumbers(Listextends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }
    // 消费者使用 ? super
    public void addNumbers(Listsuper 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。

public enum Color {
    RED, GREEN, BLUE, YELLOW, PURPLE
}
public class EnumSetExample {
    public static void main(String[] args) {
        // 创建一个空的 EnumSet
        EnumSet emptySet = EnumSet.noneOf(Color.class);
        System.out.println("Empty Set: " + emptySet);
        // 创建一个包含指定元素的 EnumSet
        EnumSet someColors = EnumSet.of(Color.RED, Color.GREEN);
        System.out.println("Some Colors: " + someColors);
        // 创建一个包含另一个集合所有元素的 EnumSet
        Set anotherSet = new HashSet<>(Arrays.asList(Color.BLUE, Color.YELLOW));
        EnumSet copiedSet = EnumSet.copyOf(anotherSet);
        System.out.println("Copied Set: " + copiedSet);
        // 创建一个包含指定范围内的所有枚举值的 EnumSet
        EnumSet 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)。

public enum DayOfWeek {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public class EnumMapExample {
    public static void main(String[] args) {
        // 创建一个空的 EnumMap
        EnumMap emptyMap = new EnumMap<>(DayOfWeek.class);
        System.out.println("Empty Map: " + emptyMap);
        // 创建一个包含初始键值对的 EnumMap
        EnumMap 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 entry : dayMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        // 创建一个包含另一个映射所有键值对的 EnumMap
        Map anotherMap = new HashMap<>();
        anotherMap.put(DayOfWeek.FRIDAY, "星期五");
        anotherMap.put(DayOfWeek.SATURDAY, "星期六");
        EnumMap copiedMap = new EnumMap<>(anotherMap);
        System.out.println("Copied Map: " + copiedMap);
    }
}

Lambda 和StreamLambda优先于匿名类

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

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

方法引用优先于Lambda

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

     import java.util.function.Consumer;
     public class ConsumerExample {
         public static void main(String[] args) {
             Consumer<String> consumer = System.out::println;
             consumer.accept("Hello, World!");
         }
     }
     

     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
         }
     }
     

     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
         }
     }
     

import java.util.function.Supplier;
     public class SupplierExample {
         public static void main(String[] args) {
             Supplier supplier = System::currentTimeMillis;
             long result = supplier.get();
             System.out.println(result); // 输出当前时间的毫秒数
         }
     }
     

    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
         }
     }
     

     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!");
         }
     }
     

     import java.util.function.BiFunction;
     public class BiFunctionExample {
         public static void main(String[] args) {
             BiFunction biFunction = Integer::sum;
             int result = biFunction.apply(3, 5);
             System.out.println(result); // 输出 8
         }
     }
     

    @FunctionalInterface
public interface MyFunction {
    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):

public static  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)。

慎用重载

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

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

慎用可变参数

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

    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个参数的再使用可变参数。

    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:

private final List 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 或其子类。

优先使用标准异常不要忽略异常

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

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