《Effective JAVA》记录

第1章:引言

  • Java编程语言的发展

本小节是关于“Java编程语言的发展”,主要介绍了Java的历史和发展,以及Java语言相较于其他编程语言的优势和局限性。其中,Java被广泛应用于企业级应用程序和互联网应用程序的开发,因其平台无关性、安全性、可靠性、易维护性和良好的性能表现而备受青睐。本小节也提到了Java的一些局限性,如运行时性能的瓶颈、内存管理的复杂性和语言本身的限制等。

第2章:创建和销毁对象

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

静态工厂方法可以提供更好的灵活性和可读性,也可以实现单例模式等常用的设计模式。

示例代码如下:

// 公有构造器
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// 静态工厂方法
public class Person {
    private String name;
    private int age;

    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static Person create(String name, int age) {
        return new Person(name, age);
    }
}

静态工厂的优势:命名灵活、可以返回子类对象、可以实现单例模式、可以缓存对象等;劣势在于不能被继承、不容易被发现。个人理解:使用静态工厂方法可以增加代码的灵活性,可以更好的控制对象的创建,以及实现一些设计模式,类如单例模式、工厂模式。

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

(1)当一个类需要传入多个参数时,使用多个构造方法会导致代码难以维护,容易出错。此时可以考虑使用构造器模式;

(2)构造器模式可以使用fluent API的方式来构造对象,增加代码的可读性和易用性;

(3)构造器模式的实现方法:创建一个静态内部类,内部类中的字段对应外部类的所有参数,内部类中的构造器方法用于构造对象,内部类中的setter方法用于设置参数的值,最终返回一个外部类的实例;

(4)构造器模式的优势:可以避免JavaBean模式中的安全性问题(对象可能在构造过程中处于不一致状态),可以使得参数设置变得清晰易懂,可以让代码具有很好的可读性和可维护性。

(5)构造器模式的劣势:需要编写额外的代码来实现构造器,代码量相对较大,需要维护更多的类和对象。

以下是一个简单的构造器模式示例代码:

public class Person {
    private final String name;
    private final int age;
    private final String address;
    private final String phone;

    private Person(PersonBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
        this.phone = builder.phone;
    }

    public static class PersonBuilder {
        private String name;
        private int age;
        private String address;
        private String phone;

        public PersonBuilder(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public PersonBuilder address(String address) {
            this.address = address;
            return this;
        }

        public PersonBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

    // getter methods

}

构造器模式可以避免多个构造器方法带来的代码难以维护的问题,使用流畅API的方式可以增加代码的可读性和易用性。此外,构造器方法还可以避免JavaBean模式中对象可能处于不一致状态的安全性问题。

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

主要讲述了如何更加优雅高效的实现单例模式,那就是使用枚举,需要编写一个包含单个元素的枚举类型。

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

一些工具类,我们提供了静态方法,不希望这个类被私有化,可以通过构造器私有化,产生的副作用就是:该类不可以被子类化,因为所有的构造器都必须显式或者隐式的调用超类构造器。

  • 避免创建不必要的对象

(1)使用基本数据类型而非装箱基本数据类型可以避免创建不必要的对象;

(2)避免使用String或BigDecimal等不可变类的构造方法创建不必要的对象;

(3)重用可重用对象,例如可以使用静态工厂方法或对象池来重用对象。

避免创建不必要的对象可以提高代码的执行效率,减少不必要的内存开销。

// 创建不必要的对象
String str = new String("hello");

// 避免创建不必要的对象
String str = "hello";

// 重用可重用对象
public static final Person EMPTY_PERSON = new Person("", 0);

  • 消除过期的对象引用

(1)及时将过期对象的引用赋值为null,可以让垃圾回收器及时回收这些对象;

(2)避免内存泄露的方法:使用WeakHashMap或SoftReference等。

// 过期对象的引用没有被清除
public static Map<String, Object> cache = new HashMap<>();

// 及时清除过期对象的引用
public static Map<String, Object> cache = new WeakHashMap<>();

即使清理过期对象的引用可以让垃圾回收器及时回收这些对象,避免内存泄漏,提高代码的可靠性和稳定性。

  • 避免使用终结方法和清除方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一帮情况下是不必要的。使用终结方法会导致行为不稳定,降低性能等问题。java语言规范不仅不保证终结方法会被及时执行,而且根本就不保证他们会被执行。

第3章:对于所有对象都通用的方法

  • 覆盖equals时总要覆盖hashCode

(1)在java中,如果两个对象相等,则它们的hashCode值必须相等;

(2)覆盖equals方法时,必须同时覆盖hashCode方法,以便于对象能够正确地放入散列表等基于哈希地数据结构中;

(3)hashCode方法的实现应该包括类中所有重要字段的散列值,如果类中没有重要字段,可以返回一个常量值;

(4)在覆盖hashCode方法时,要确保每个包含在equals方法比较中的字段都包含在hashCode方法中的计算中;

(5)不要试图从hashCode方法中排除一个对象中的某些字段,否则可能会导致hashCode方法和equals方法之间的不一致。

以下是一个覆盖equals和hashCode方法的示例代码:

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

覆盖equals方法时,必须同时覆盖hashCode方法,以便于对象能够正确地放入散列表等基于哈希的数据结构中,hashCode方法的实现应该包括类中所有重要字段的散列值,如果类中没有重要字段,可以返回一个常量值。覆盖hashCode方法时,要确保每个包含在equals方法比较中的字段都包含在hashCode方法中的计算中,不要试图从hashcode方法中排除一个对象中的重要字段。

  • 始终要覆盖toString

(1)对象的toString方法应该返回一个简洁而有意义的字符串,以便于人们可以轻松地理解这个对象的内容。

(2)toString方法应该包括对象的重要信息,包括所有字段的值,如果对象有数组字段,则可以使用Arrays.toString方法。

(3)不要试图从toString方法返回一个过于详细的字符串,因为这样可能会影响应用程序的性能。

(4)始终要覆盖toString方法,因为这是调试和日志记录的重要工具。

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}
  • 谨慎地覆盖clone

  1. Java中的Cloneable接口是一个标记接口,表示该类的实例可以被克隆。
  2. 如果要实现克隆功能,类必须实现Cloneable接口并重写Object的clone方法。注意,clone方法是受保护的,因此需要在子类中重新声明为public方法。
  3. 通常情况下,覆盖clone方法的类应该使用super.clone来创建并返回一个新的实例。这样做的好处是不会破坏类的约束条件,而且可以自动处理大多数字段类型。
  4. 为了确保克隆方法返回的对象和原始对象具有相同的值,应该逐个复制每个字段。对于非基本类型的字段,应该使用它们自己的克隆方法或复制构造函数。
  5. 覆盖clone方法可能会导致一些问题,例如会违反约束条件,导致克隆出来的对象与原始对象不一致,因此在覆盖clone方法时必须非常小心。
  6. 一般来说,最好使用其他的复制方式,例如复制构造函数或者静态工厂方法,而不是直接覆盖clone方法。

以下是一个示例代码:

public class Person implements Cloneable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

Cloneable接口是一个标记接口,用来表示该类的实例可以被克隆。如果要实现克隆功能,类必须实现Cloneable接口并重写Object的clone方法。一般情况下,应该使用super.clone来创建并返回一个新的实例,然后逐个复制每个字段。覆盖clone方法可能会导致一些问题,因此在覆盖clone方法时必须非常小心,最好使用其他的复制方式。

  • 考虑实现Comparable接口

  1. Comparable接口定义了一个比较方法compareTo,用于对对象进行自然排序。
  2. 如果类的实例可以按照某个顺序进行排序,则应该实现Comparable接口。
  3. 实现Comparable接口时,应该确保compareTo方法与equals方法保持一致,即对于相等的对象,compareTo方法应该返回0。
  4. compareTo方法应该与equals方法的定义一致,具有自反性、对称性和传递性,否则可能导致意外的行为。
  5. 实现Comparable接口时,应该尽可能地让类的自然排序与equals方法保持一致。如果无法做到这一点,就应该在文档中明确说明。

例如:

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(age, other.age);
    }
}

实现Comparable接口可以使类的实例可以按照某个顺序进行排序,可以方便地使用Java集合框架中的排序算法。在实现Comparable接口时,应该确保compareTo方法与equals方法保持一致,即对于相等的对象,compareTo方法应该返回0,并且应该满足自反性、对称性和传递性。

第4章:类和接口

  • 最小化类和成员的访问权限

  1. 访问权限是控制类和成员的可见性的机制。
  2. Java有4种访问权限:public、protected、default(即没有修饰符)和private。
  3. 如果不需要从包的外部访问一个类,就应该将其声明为包私有(即默认访问权限),这样可以最小化类的可访问性,减少类被误用的可能性。
  4. 如果一个方法不需要从子类或其他包中访问,就应该将其声明为private,这样可以最小化该方法的可访问性,减少方法被误用的可能性。
  5. 如果一个成员变量不需要从子类或其他包中访问,就应该将其声明为private,并提供访问方法(即getter和setter)来控制其访问。
  6. 将成员变量声明为public是一种很糟糕的做法,因为这会使类失去封装性,导致代码的耦合性增加,难以维护和扩展。
  7. 对于一些不可变的类,最好将所有的成员变量声明为final,并在构造函数中初始化,这样可以确保类的不可变性。
  8. 避免使用protected修饰符,因为它打破了封装性,并允许子类从类的外部访问父类的成员变量。
  9. 尽可能地使类和成员的访问权限最小化,这有助于降低代码的耦合性,增加代码的可维护性和可扩展性。

个人理解:最小化类和成员的访问权限可以帮助我们降低代码的耦合性,增加代码的可维护性和可扩展性。在设计类和成员时,应该尽可能地将它们的访问权限最小化,只允许必要的访问,并提供适当的访问方法来控制其访问。

  • 在公有类中使用访问方法而非公有域

  1. 在设计类的时候,应该尽量隐藏其内部细节,避免直接暴露类的实现细节。
  2. 公有类中使用公有域虽然很方便,但这会使类失去封装性,导致代码的耦合性增加,难以维护和扩展。
  3. 应该使用访问方法(即getter和setter)来控制类的成员变量的访问,而不是直接暴露公有域。
  4. 使用访问方法可以提高类的灵活性,例如可以在getter中添加对成员变量的校验或转换逻辑,而不影响类的外部使用。
  5. 可以使用包级私有(default)访问权限来允许类的内部访问其成员变量,但同时避免了类的实现细节被其他包访问。
  6. 在Java 9之前,如果一个类只有一个包含单个元素的私有嵌套类(例如函数对象或策略对象),可以考虑使用私有域而非访问方法来暴露这个单个元素,这样可以提高代码的简洁性。
  7. 总之,应该尽可能地隐藏类的实现细节,最小化类和成员的访问权限,并使用访问方法来控制类的成员变量的访问。

示例代码:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 使用访问方法控制name的访问
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // 使用访问方法控制age的访问
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

  • 使可变性最小化

  1. 什么是不可变类?

    不可变类指的是其实例的状态在创建之后就不能再被修改的类。在不可变类中,所有字段都是final修饰的,并且没有setter方法或其他可以修改对象状态的方法。不可变类中的所有方法都不会修改对象状态,它们只是返回新的对象或者已存在的对象的某些信息。

  2. 为什么要将类设计成不可变类?

    不可变类有以下优点:

    • 线程安全:由于不可变类的状态是不可变的,因此它们可以安全地在多个线程中共享,不需要进行同步操作。
    • 简单性:不可变类的代码更加简单,易于理解和维护。
    • 安全性:不可变类避免了由于修改对象状态而导致的意外错误,使得程序更加可靠。
    • 可复用性:由于不可变类可以被安全地共享,因此它们可以更容易地被复用。
  3. 如何设计不可变类?

    • 将所有字段都声明为final类型,并在构造函数中初始化它们。
    • 不要提供修改对象状态的方法,包括setter方法、修改字段的方法以及其他可以修改对象状态的方法。
    • 如果需要提供修改对象状态的方法,可以返回新的对象而不是修改原对象状态。
    • 如果类中包含可变对象,要确保在返回它们的时候进行防御性拷贝,以防止对象状态被修改。

    下面一段代码是设计了一个不可变的Person类:

public final class Person {
    private final String name;
    private final int age;
    private final Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = new Address(address.getStreet(), address.getCity(), address.getState(), address.getZipCode());
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return new Address(address.getStreet(), address.getCity(), address.getState(), address.getZipCode());
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }
}

public final class Address {
    private final String street;
    private final String city;
    private final String state;
    private final String zipCode;

    public Address(String street, String city, String state, String zipCode) {
        this.street = street;
        this.city = city;
        this.state = state;

  • 复合优先于继承

这个主题的核心思想是,在设计类时,优先考虑使用复合(composition)而不是继承(inheritance),因为复合比继承更加灵活,更容易维护和扩展。以下是一些“复合优于继承”原则的细节和示例代码:

1.继承的缺点

  • 继承打破了封装性:子类可以访问父类的私有字段和方法,导致代码的可维护性降低。
  • 继承是静态的:继承关系在编译时就确定下来了,无法在运行时进行修改。这导致在扩展和修改代码时受到限制。
  • 继承可能会导致脆弱的基类:当基类的实现发生变化时,子类可能会受到影响,需要重新编译和测试。

2.使用复合的优点

  • 复合可以在运行时进行修改:组合关系可以在运行时动态地修改,因此更加灵活。
  • 复合可以保持封装性:通过使用private字段和方法,可以保持类的封装性。
  • 复合可以被设计为不可修改的:如果将组合对象声明为final,那么就可以确保它们不会被修改。

3.示例代码

下面是一个简单的例子,是如何使用复合而不是继承来实现一个栈(Stack)类。在这个例子中,使用了一个LinkedList来存储元素,而不是通过继承ArrayList来实现栈。

public class Stack<E> {
    private LinkedList<E> list = new LinkedList<>();

    public void push(E e) {
        list.addFirst(e);
    }

    public E pop() {
        return list.removeFirst();
    }

    public E peek() {
        return list.getFirst();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }

    public int size() {
        return list.size();
    }
}

上述例子中,使用了LinkedList作为组合对象,而不是继承它来实现栈。这种方式使得栈类更加灵活,因为可以在运行时更改它的组合对象,例如,可以将LinkedList替换为一个ArrayDeque或者一个普通的数组来实现不同的行为。

  • 接口优先于抽象类

本节阐述了使用接口和抽象类的优缺点,并提出了使用接口的一些最佳实践。

使用接口的优点:

  1. 灵活性:接口使得实现类能够灵活地实现多个接口,从而实现更灵活的设计和更高的可复用性。
  2. 透明性:接口定义了实现类的公共方法和行为,使得使用者能够更加清晰地理解和使用这些类。
  3. 安全性:接口可以限制实现类的行为,从而提高代码的安全性和可靠性。

使用抽象类的优点:

  1. 可以提供默认实现:抽象类可以提供一些默认实现,减少了实现类的工作量。
  2. 更容易进行代码重构:当抽象类需要增加新的方法时,可以提供默认实现,而不需要修改所有的实现类。

在使用接口和抽象类时,最佳实践包括:

  1. 优先使用接口:因为接口可以提供更大的灵活性和透明性。
  2. 如果需要提供默认实现或者共享代码,可以使用抽象类。
  3. 如果需要限制实现类的行为或者提供强制性的行为,可以使用接口。
  • 为后代设计接口

这一小节主要讲述了如何设计接口以便于后续的扩展和维护。

1.接口应该精简并且富有表现力。

  • 接口中不应该过多地暴露方法,应该只暴露必要的方法。
  • 每个方法应该尽量描述清楚自己的用途,但是不应该过度地描述,导致难以实现和维护。

2.接口应该被设计成容易被实现。

  • 接口的实现应该是自然而然的,而不应该需要实现者过度地了解接口内部的细节。
  • 接口应该被分层以便于实现者逐渐掌握它们的复杂度。

3.接口应该演化,以便于支持未来的需求。

  • 接口的设计应该考虑到未来可能出现的新需求。

  • 接口应该被定义成稳定的,以便于后续的扩展和维护。

  • 接口只用于定义类型

该小节主要强调了接口在java中的作用,即为了定义类型而存在,而不是用于实现某中行为。

  1. 接口只用于定义类型,而不是用于实现某些行为。
  2. 实现接口的类必须提供接口中定义的所有方法。
  3. 接口常常用来定义常量,如下所示:
public interface Constants {
    public static final int MAX_THREADS = 10;
    public static final String DEFAULT_NAME = "John Doe";
}

​ 4.接口常常用来定义嵌套类,如下所示:

public interface Shape {
    public static class Circle implements Shape {
        private double radius;

        public Circle(double radius) {
            this.radius = radius;
        }

        public double area() {
            return Math.PI * radius * radius;
        }
    }

    public static class Rectangle implements Shape {
        private double width;
        private double height;

        public Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        }

        public double area() {
            return width * height;
        }
    }

    public double area();
}

5.接口可以用于定义函数式接口,这是Java 8中的新特性。函数式接口是一个只有一个抽象方法的接口。例如:

@FunctionalInterface
public interface MathOperation {
    int operate(int a, int b);
}

在java8中,Lambda表达式可以用于创建函数式接口的实例。

总之,接口是Java中的一个重要概念,主要用于定义类型。在设计接口时,应该注意以下几点:接口只用于定义类型,实现接口的类必须提供接口中定义的所有方法,接口可以用于定义常量和嵌套类,以及接口可以用于定义函数式接口。

  • 类层次优于标签类

类层次结构是指一组类,这些类通过继承关系组成了一个树形结构。这些结构的好处在于可以通过继承和多态性实现对多种类型对象的处理,避免使用标签类(tagged class)等不稳定和不安全的做法。

标签类是指一个类中包含了一个tag域(通常是enum类型),根据tag的不同取值,确定该对象的类型。标签类通常需要使用switch语句等方式来对不同的tag值进行不同的处理,这会导致代码的可读性和可维护性变差。

与标签类相比,使用类层次结构可以更好地利用继承和多态性实现对多种类型对象的处理。

  • 用函数对象表示策略

本小节主要介绍了一种设计模式——策略模式,即用函数对象表示策略,将算法的实现与算法的使用进行分离。在实现策略模式时,需要定义一个接口,包含一个方法,表示算法的执行,然后再实现该接口的多个类,每个类表示一个不同的算法实现。在需要使用该算法时,将对应的算法实现类的实例传递给该方法,即可实现算法的执行。

具体实现时,可以使用Lambda表达式或者匿名内部类来实现函数对象,从而实现策略模式。

示例代码:

public interface Strategy {
    int calculate(int a, int b);
}

public class AddStrategy implements Strategy {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
}

public class SubtractStrategy implements Strategy {
    @Override
    public int calculate(int a, int b) {
        return a - b;
    }
}

public class Calculator {
    private Strategy strategy;
    
    public Calculator(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public int execute(int a, int b) {
        return strategy.calculate(a, b);
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(new AddStrategy());
        int result = calculator.execute(1, 2);
        System.out.println(result); // 输出 3
        
        calculator = new Calculator(new SubtractStrategy());
        result = calculator.execute(3, 1);
        System.out.println(result); // 输出 2
        
        // 使用Lambda表达式实现策略模式
        calculator = new Calculator((a, b) -> a * b);
        result = calculator.execute(2, 3);
        System.out.println(result); // 输出 6
    }
}

在该示例代码中,定义了一个Strategy接口,表示策略,其中包含一个calculate方法,表示算法的执行。然后,定义了AddStrategySubtractStrategy两个类,分别表示加法和减法算法的实现。Calculator类表示计算器,其中包含一个strategy成员变量,表示使用的策略,通过构造函数来传入策略实现类的实例,然后在execute方法中调用对应的策略实现类的calculate方法,从而实现算法的执行。

Main类中,先创建一个使用加法算法的计算器,然后调用execute方法执行算法,输出结果。接着,再创建一个使用减法算法的计算器,同样调用execute方法执行算法,输出结果。最后,使用Lambda表达式来实现乘法算法,同样调用execute方法执行算法,输出结果。

通过策略模式的应用,可以方便地替换算法实现,同时实现算法实现与算法使用的分离,提高代码的可维护性和可扩展性。

  • 优先考虑静态成员类

本小节主要讲述了如何使用静态成员类来提高代码的可读性、安全性和性能,并提出了静态成员类优于非静态成员类的原因。

静态成员类是在另一个类的内部定义的类,与外部类没有依赖关系,可以单独存在。使用静态成员类有以下优点:

  1. 提高代码的可读性:将相关的类组合在一起,有助于理解代码的意图。
  2. 提高代码的安全性:将内部类的实例的创建和访问限制在外部类中,可以防止外部类之外的代码访问内部类实例或修改其状态。
  3. 提高代码的性能:静态成员类可以被当作普通类进行加载和使用,与外部类并没有耦合关系,可以提高代码的灵活性和可维护性。

第5章:泛型

  • 优先考虑泛型方法

该小节主要介绍了在编写泛型类时,应该优先考虑使用泛型方法而不是将类型参数添加到整个类中。这样有利于代码的可读性和可维护性,并且使得代码更加灵活。

1.在编写泛型类时,应该优先考虑泛型方法;

2.泛型方法可以使得代码更加通用;

3.在使用泛型方法时,可以通过类型推断来简化代码;

4.泛型方法可以使用类型参数来限制类型:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

以上泛型方法中使用了一个类型参数T,该参数被限制为实现了Comparable接口的类型,这样就可以在方法中使用compareTo方法来比较对象。

5.泛型方法还可以使用通配符类型来增加灵活性:

public static void printList(List<?> list) {
    for (Object elem : list)
        System.out.print(elem + " ");
    System.out.println();
}

上面这个例子中,泛型方法使用了通配符类型?,可以接受任何类型的List对象,这样就可以使得代码更加灵活。

  • 列表优先于数组

1.数组的优点

数组是Java中的一种基本类型,使用时需要指定元素类型和数组长度。在实际使用中,数组有以下缺点:

  • 数组的长度固定,无法动态添加或删除元素。
  • 数组不支持类型参数化,即无法声明泛型数组,如T[]。
  • 数组与泛型结合时会出现类型擦除的问题,即泛型数组会被擦除为Object[]类型,无法避免强制类型转换的问题。

2.列表的优点

相比之下,列表则是一种更为灵活的数据结构,可以动态添加和删除元素,支持类型参数化,可以声明泛型列表,如List。另外,列表还提供了一系列方便的方法,如添加元素、删除元素、获取元素等。

3.优先使用列表

由于列表的灵活性和泛型支持,通常情况下应优先考虑使用列表而不是数组。在实际编程中,我们应该尽可能使用Java集合框架中提供的列表类,如ArrayList、LinkedList等。

  • 优先考虑泛型类型

在Java 5之前,集合框架使用原生态类型来存储对象,这些原生态类型并没有提供编译时类型安全的保证,而且必须在运行时进行显式的强制类型转换。Java 5中引入了泛型,可以为Java程序员提供编译时类型安全的保证。使用泛型类型可以提高代码的清晰度和安全性。

在Java中,泛型类型和原生态类型之间有一个重要的区别:泛型类型可以在编译时进行类型检查,而原生态类型则不能。这意味着泛型类型可以捕获类型错误,而原生态类型不能。因此,当使用集合等参数化类型时,应该优先考虑泛型类型。

比较使用原生泛型和泛型类型的不同之处,示例代码:

import java.util.ArrayList;
import java.util.List;

public class GenericTypeExample {

    public static void main(String[] args) {
        // 使用原生态类型List
        List list = new ArrayList();
        list.add("Hello");
        list.add(123);
        list.add(true);
        // 需要强制类型转换
        String str = (String) list.get(0);

        // 使用泛型类型List<String>
        List<String> strList = new ArrayList<>();
        strList.add("Hello");
        // 编译时错误,类型不匹配
        // strList.add(123);
        // 编译时错误,类型不匹配
        // strList.add(true);
        // 不需要强制类型转换
        String s = strList.get(0);
    }
}

上面的示例中,使用原生态类型的List需要在运行时进行强制类型转换,容易出现类型错误,而使用泛型类型的List则可以在编译时捕获类型错误,从而提高代码的安全性和可读性,因此,应该尽可能地使用泛型类型,而避免使用原生态类型。

  • 使可变参数列表与泛型方法一起使用时要非常小心

在Java 5引入泛型之前,Java使用可变参数列表(varargs)机制实现了printf方法。当泛型和可变参数列表一起使用时,编译器可能会生成一些不可预测的代码,这可能会导致运行时异常。

  • 优先考虑类型安全的异构容器

异构容器是一种键值对映射,其中键和值可以是任何类型。通常,我们使用一个非泛型的Map来实现异构容器,例如:

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();
    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

在这个例子中,我们使用了一个非泛型的Map来存储键值对,其中键的类型是Class<?>,而值的类型是Object。我们使用putFavorite方法将一个对象实例与一个Class对象关联起来,使用getFavorite方法从Map中获取与指定类型相关联的对象实例。注意,getFavorite方法使用type.cast方法将Object对象强制转换为指定类型的对象。

这种方法的缺点是需要手动进行强制类型转换,这可能导致运行时异常。另外,我们不能保证从Map中获取的对象实例的类型与指定类型完全匹配。因此,如果可能的话,我们应该使用类型安全的异构容器。

第6章:枚举和注解

  • 用枚举类型来实现Singleton属性

Singleton是指只能创建一个实例的类。在Java中,实现Singleton有多种方式,其中一种方式是使用枚举类型。枚举类型保证只有一个实例,并且是在枚举类型被加载的时候自动创建的。此外,枚举类型的实现方式也是线程安全的。

  • 用EnumSet代替位域

位域是指用一个整数值的不同二进制位来表示多个开关状态的一种技术。在Java中,位域的实现方式往往会带来很多问题,比如代码可读性不高、易出错等。而Java中提供了EnumSet类,可以用来代替位域,使代码更加可读性高,易于维护。

以下是用EnumSet代替位域的示例代码:

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
    public void applyStyles(Set<Style> styles) {
        //...
    }
}

在Text类中,我们用EnumSet代替了位域来表示不同的字体样式。applyStyles方法接收一个EnumSet类型的参数styles,用来表示应用的字体样式。这样,代码就变得更加可读性高了,易于维护。

  • 用接口模拟可伸缩的枚举

Java中的枚举类型是一种非常强大的工具,但有时候我们需要更灵活的枚举类型,比如在编译时不确定所有的可能值,或者需要在运行时动态添加或删除枚举常量。这种情况下,我们可以使用接口来模拟可伸缩的枚举。

具体实现的思路是:定义一个接口,接口中包含一个方法来获取枚举常量的名称和一个方法来获取该枚举常量的序号。然后定义实现该接口的类来代表枚举常量,每个类中包含该枚举常量的名称和序号,还可以在类中添加一些其他的属性和方法。

// 定义枚举类型接口
public interface EnumExample {
    String getName();
    int getOrdinal();
}

// 实现枚举常量
public class EnumExampleImpl implements EnumExample {
    private String name;
    private int ordinal;
    
    public EnumExampleImpl(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    
    public String getName() {
        return name;
    }
    
    public int getOrdinal() {
        return ordinal;
    }
    
    // 定义一些其他属性和方法
}

// 定义一个包含枚举常量的类
public class EnumContainer {
    public static final EnumExample A = new EnumExampleImpl("A", 0);
    public static final EnumExample B = new EnumExampleImpl("B", 1);
    public static final EnumExample C = new EnumExampleImpl("C", 2);
    
    private EnumContainer() {}
    
    // 获取所有枚举常量
    public static List<EnumExample> values() {
        return Arrays.asList(A, B, C);
    }
    
    // 根据名称获取枚举常量
    public static EnumExample valueOf(String name) {
        for (EnumExample e : values()) {
            if (e.getName().equals(name)) {
                return e;
            }
        }
        throw new IllegalArgumentException("No enum constant with name " + name);
    }
    
    // 根据序号获取枚举常量
    public static EnumExample valueOf(int ordinal) {
        for (EnumExample e : values()) {
            if (e.getOrdinal() == ordinal) {
                return e;
            }
        }
        throw new IllegalArgumentException("No enum constant with ordinal " + ordinal);
    }
}

这个示例代码中,EnumExample是定义枚举类型的接口,EnumExampleImpl是实现枚举常量的类,EnumContainer是包含枚举常量的类。EnumContainer中定义了获取所有枚举常量、根据名称获取枚举常量。

  • 用注解代替命名模式

命名模式是指用命名的方式表示某些属性或特性,例如Java中的@Deprecated注解就是一种命名模式。使用注解可以代替命名模式,有以下好处:

  • 注解可以提供更多的信息,例如默认值、参数等;

  • 注解可以在运行时被读取,从而进行动态处理;

  • 注解可以被组合使用。

  • 标记接口优于标记注解

标记接口是没有方法或常量声明的接口,它的唯一目的是充当某个类或接口具有某些特征的标记。相反,标记注解是一个空的注解类型,标记注解的唯一目的是指示注解类型的存在。

本节建议在需要为类型打标记时优先使用标记接口而不是标记注解。这是因为使用标记接口具有以下优点:

  1. 标记接口定义了一个新的命名类型,因此它们可以用于任何可以使用类型的地方,例如类和方法的参数和返回类型。
  2. 标记接口可以被强制要求由实现它们的类实现。这可以在编译时强制执行类型检查。

第7章:方法

  • 检查参数的有效性

在编写方法时,应该始终检查传入参数的有效性。这样做可以避免方法被传入无效的参数,提高代码的健壮性和可靠性。

检查参数的有效性有多种方式,可以使用断言(assert)、异常、日志等方式。对于公共方法,最好使用异常来检查参数的有效性,这样可以在发生异常时将问题暴露出来,让调用方进行修复。

  • 必要时进行保护性拷贝

在编写方法时,如果方法的参数是可变对象,为了防止该可变对象被不当地修改,可以对该对象进行保护性拷贝(defensive copying)。这样做可以增强代码的健壮性和可靠性。

以下是对可变对象进行保护性拷贝的示例代码:

public void setValues(List<Integer> values) {
    this.values = new ArrayList<>(values);
}

在这个示例中,使用new ArrayList<>(values)values进行保护性拷贝,以防止在该方法外部修改values对对象的值。

需要注意的是,在进行保护性拷贝时,需要考虑可变对象内部的可变性。如果可变对象内部包含可变对象,那么对于该可变对象,也需要进行保护性拷贝。

  • 谨慎设计方法签名

方法签名指的是方法名和参数类型列表。在设计方法签名时,需要注意以下几点:

  1. 避免使用过长或者易混淆的方法名,建议使用明确简洁的命名规则。
  2. 慎用重载,避免在参数类型或数量上进行过多的变化。
  3. 参数数量不宜过多,最好不超过4个。
  4. 对于布尔类型的参数,最好避免使用“is”作为前缀。

示例代码:

// 1. 避免使用过长或者易混淆的方法名
// 不好的写法
public void f() {}
public void calculateSumOfIntegersFromGivenList() {}

// 建议的写法
public void process() {}
public void calculateSum(List<Integer> integers) {}

// 2. 慎用重载
// 不好的写法
public void print(String message) {}
public void print(Integer num) {}
public void print(Double num) {}

// 建议的写法
public void print(String message) {}
public void printNumber(Number num) {}

// 3. 参数数量不宜过多
// 不好的写法
public void process(String arg1, String arg2, String arg3, String arg4, String arg5) {}

// 建议的写法
public void process(Data data) {}

// 4. 对于布尔类型的参数,最好避免使用“is”作为前缀
// 不好的写法
public void setUserActive(boolean isActive) {}

// 建议的写法
public void setUserState(UserState state) {}
  • 慎用重载

方法重载可以增强代码的灵活性,但过度使用会导致代码难以理解和维护。重载方法的参数类型和数量应该保持简单和一致。

  • 慎用可变参数

可变参数可以方便地接收不同数量的参数,但是也容易导致代码难以理解和出现类型不匹配的问题。使用可变参数时应该尽量避免使用原始类型,而是使用包装类型或者Object类型。

  • 返回零长度的数组或集合,而不是null

返回null会使得调用者需要进行额外的判断,增加代码的复杂度。如果方法返回的是数组或者集合,可以返回一个零长度的数组或集合,这样就避免了null的问题。

  • 为所有导出的API元素编写文档注释

在设计API时,为所有导出的API元素编写文档注释是非常重要的。好的文档注释可以提高代码的可读性和易用性,让用户更容易理解和使用API。

以下是一些编写文档注释的最佳实践:

  1. 对于公共API,注释要尽量详细,包括参数说明、返回值说明、异常说明等。
  2. 对于非公共API,注释可以相对简洁,但也需要说明方法的作用和使用方法。
  3. 在编写注释时,要注意注释和代码的同步更新,确保注释的准确性和时效性。
  4. 使用Javadoc注释规范,便于自动生成API文档。

第8章:通用程序设计

  • 尽量少地受检查的异常

本小节讲述了关于异常的一些设计原则和最佳实践,主要内容包括:

  1. 尽量少地受检查的异常:异常分类有两种:受检查异常和运行时异常。受检查异常必须在方法签名中声明,调用者必须显式处理或者继续抛出该异常,否则代码就无法编译通过。因此,受检查异常的使用应该非常谨慎,应该只在必要的时候使用。例如,当一个方法无法根据方法的约束条件返回一个有效的返回值时,可以抛出受检查异常。
  2. 对可恢复的情况使用受检查的异常,对编程错误使用运行时异常:如果调用者可以通过采取恰当的措施来使操作成功,则应使用受检查异常。否则,应该使用运行时异常。运行时异常是不受检查的,因此不需要在方法签名中声明。如果方法抛出运行时异常,则调用者可以选择是否捕获并处理该异常。一般情况下,运行时异常通常用于表示程序的内部错误、不变量被破坏或参数不合法等问题。
  • 避免不必要地使用受检查的异常

Java异常分为受检查的异常和未受检查的异常。受检查的异常指需要在方法中声明并处理的异常,比如IOException和SQLException等。而未受检查的异常则不需要声明和处理,比如NullPointerException和IndexOutOfBoundsException等。

受检查的异常在某些情况下确实是有必要的,比如在需要对异常进行恢复和处理的情况下。但是在某些情况下,使用受检查的异常可能会增加代码的复杂性和不必要的限制,甚至会导致一些不良的编程实践。

在编写代码时,应该尽可能避免使用不必要的受检查异常,可以使用未受检查异常或者返回特殊的错误值来代替。这样可以使代码更加简洁明了,而且也不会给程序员带来额外的负担。

// 不必要地使用受检查的异常
public List<String> readFile(String filePath) throws IOException {
    File file = new File(filePath);
    BufferedReader reader = new BufferedReader(new FileReader(file));
    List<String> lines = new ArrayList<>();
    String line;
    while ((line = reader.readLine()) != null) {
        lines.add(line);
    }
    return lines;
}

// 使用未受检查的异常
public List<String> readFile(String filePath) {
    try {
        File file = new File(filePath);
        BufferedReader reader = new BufferedReader(new FileReader(file));
        List<String> lines = new ArrayList<>();
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
        return lines;
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

// 返回特殊的错误值
public List<String> readFile(String filePath) {
    File file = new File(filePath);
    if (!file.exists()) {
        return Collections.emptyList();
    }
    try {
        BufferedReader reader = new BufferedReader(new FileReader(file));
        List<String> lines = new ArrayList<>();
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
        return lines;
    } catch (IOException e) {
        return Collections.emptyList();
    }
}
  • 抛出与抽象相对应的异常

在Java中,异常是用来表示程序运行过程中出现的错误或异常情况的。在抛出异常时,应该抛出与抽象相对应的异常,而不是具体的实现细节。

具体来说,如果在方法中出现了异常情况,应该抛出对应的抽象异常,而不是具体的实现异常。这样可以使代码更加通用,提高代码的可复用性和可维护性。

  • 在细节消息中包含失败-捕获信息

在程序抛出异常时,要让调用者了解失败的原因,以便能够更好地处理问题。在捕获异常时,如果只将异常消息记录到日志文件中,那么调试时将很难定位异常的根本原因。为了提高日志的可读性,最好将异常信息的所有相关细节都记录在日志中,包括捕获异常时的参数值和其他上下文信息。可以通过String.format方法和%s占位符来记录这些细节,如下所示:

public class ExceptionTest {
    public static void main(String[] args) {
        int num = 0;
        try {
            int result = 10 / num;
            System.out.println(result);
        } catch (ArithmeticException e) {
            String errorMsg = String.format("Failed to calculate result, num = %d", num);
            System.err.println(errorMsg);
            throw new ArithmeticException(errorMsg);
        }
    }
}

在上面的示例中,除数为零导致抛出ArithmeticException异常。在异常处理程序中,将捕获到的异常信息与其他细节一起记录到日志中。如果没有捕获该异常并将其记录到日志中,调用方就无法知道发生了什么。

  • 务必要定义失败时安全的方法

编写方法时,应该考虑到所有的使用情况。如果方法会在特定的参数组合下抛出异常,那么最好在方法文档中明确说明这一点。但是,有时候可能无法避免某些条件下方法的异常。在这种情况下,可以考虑让方法始终保持失败安全,以避免可能的数据损坏或不一致性。

  • 最小化局部变量的作用域

在编写代码时,如果一个变量只被用在了一个很小的范围内,那么应该尽量缩小它的作用域。这种做法可以提高程序的可读性和可维护性,并且有时也能提高程序的性能。举个例子,考虑如下代码:

public class MyClass {
    public void myMethod() {
        String str = "Hello World!";
        if (someCondition) {
            System.out.println(str);
        }
        //...
    }
}

在这个方法中,变量str只被用在了if语句中,因此可以将它的声明放到if语句的内部,从而减小它的作用域:

public class MyClass {
    public void myMethod() {
        if (someCondition) {
            String str = "Hello World!";
            System.out.println(str);
        }
        //...
    }
}

这样做的好处是,可以让读代码的人更容易地看出变量的作用范围,也能够减少变量名的冲突。此外,局部变量的生命周期也会更短,对垃圾回收的效率也有一定的提升。

  • 在所有可能的情况下使用for-each循环

在Java 5中,引入了一种新的循环语法:for-each循环。这种循环语法可以方便地遍历数组、集合等容器类型。使用for-each循环可以让代码更简洁、更易读,也能够避免循环索引的错误。举个例子,考虑如下代码:

List<String> list = new ArrayList<>();
//...
for (int i = 0; i < list.size(); i++) {
    String str = list.get(i);
    System.out.println(str);
}

这个循环可以改写成for-each循环的形式:

List<String> list = new ArrayList<>();
//...
for (String str : list) {
    System.out.println(str);
}

使用for-each循环的好处是,可以避免循环索引的错误,并且可以使代码更加简洁易懂。此外,for-each循环还支持在遍历过程中进行元素的删除和替换操作,这对于集合类型的容器非常有用。

  • 了解和使用库

本小节主要提出了以下几点建议:

  1. 在使用第三方库时,应该选择可靠的、有好的口碑的库,避免使用不成熟或者已经过时的库。
  2. 在编写自己的库时,应该提供良好的文档,包括库的功能、使用方法和注意事项等。
  3. 在使用和编写库时,应该注重易用性和灵活性的平衡。易用性是指库的使用方法简单明了,灵活性是指库的接口设计要具有可扩展性,方便用户根据自己的需要进行扩展。

下面是一个示例,展示如何使用Java标准库中的java.util.regex包来进行字符串匹配:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexDemo {
    public static void main(String[] args) {
        String input = "hello world, my name is John.";
        Pattern pattern = Pattern.compile("\\w+");
        Matcher matcher = pattern.matcher(input);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

上面的代码使用了Pattern和Matcher两个类来进行字符串匹配,其中Pattern.compile方法编译正则表达式,返回一个Pattern对象,Matcher.group方法返回匹配的子串。这个示例展示了如何使用Java标准库中的类来完成字符串匹配的功能,这个功能也可以使用第三方库如Apache的Commons Lang库来实现。在实际开发中,需要根据具体的需求选择使用不同的库。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值