Try using it in your preferred language.

English

  • English
  • 汉语
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar
translation

这是AI翻译的帖子。

제이온

[Effective Java] 項目 1. 考慮使用靜態工廠方法而非建構函式

  • 写作语言: 韓国語
  • 基准国家: 所有国家 country-flag

选择语言

  • 汉语
  • English
  • Español
  • Bahasa Indonesia
  • Português
  • Русский
  • 日本語
  • 한국어
  • Deutsch
  • Français
  • Italiano
  • Türkçe
  • Tiếng Việt
  • ไทย
  • Polski
  • Nederlands
  • हिन्दी
  • Magyar

durumis AI 总结的文章

  • 靜態工廠方法是一種替換建構函式來建立類別實例的方法,具有指定實例名稱、快取建立成本高的物件以提高效能,以及根據需要返回不同子類別的物件等優點。
  • 特別是,在 Java 8 之前,無法在介面中宣告靜態方法,因此需要建立伴隨類別來定義靜態工廠方法。然而,從 Java 8 開始,可以在介面中直接新增靜態方法,因此不再需要單獨定義伴隨類別。
  • 使用靜態工廠方法可以促進組合而非繼承,並且為了建立不可變型別,必須遵守此限制,這反而可能成為優點。但是,與建構函式不同,靜態工廠方法在 API 說明中並不顯眼,因此開發人員需要撰寫良好的 API 文件,並遵循廣為人知的慣例命名方法,以減輕此問題。

概述

获得类的实例的传统方法是使用公共构造函数。


public class Member {

    private String name;

    private int age;

    private String hobby;

    private MemberStatus memberStatus;

    public Member(String name, int age, String hobby, MemberStatus memberStatus) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
        this.memberStatus = memberStatus;
    }
}

public enum MemberStatus {

    ADVANCED,
    INTERMEDIATE,
    BASIC;


通常,仅使用公共构造函数就足够了,但除了构造函数外,还提供静态工厂方法 (static factory method) 在某些情况下,可以让用户更容易地根据需要创建实例。


静态工厂方法的典型示例是 Boolean 的 valueOf() 方法。


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


上述方法接收基本类型 boolean 值,并将其转换为 Boolean 对象后返回。


静态工厂方法的优点

可以有名称

传递给构造函数的参数以及构造函数本身无法完全描述返回对象的特性。例如,在上面的 Member 类中,仅通过主构造函数 (name, age, hobby, memberStatus) 很难确定 Member 的特性。


此外,一个签名只能创建一个构造函数,而静态工厂方法可以有名称,因此一个签名可以创建多个静态工厂方法来返回实例。


public class Member {

    private String name;

    private int age;

    private String hobby;

    private MemberStatus memberStatus;

    public Member(String name, int age, String hobby, MemberStatus memberStatus) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
        this.memberStatus = memberStatus;
    }

    public static Member basicMember(String name, int age, String hobby) {
        return new Member(name, age, hobby, MemberStatus.BASIC);
    }

    public static Member intermediateMember(String name, int age, String hobby) {
        return new Member(name, age, hobby, MemberStatus.INTERMEDIATE);
    }

    public static Member advancedMember(String name, int age, String hobby) {
        return new Member(name, age, hobby, MemberStatus.ADVANCED);
    }


与使用构造函数区分 MemberStatus 相比,创建具有相同签名的多个静态工厂方法,可以让用户在无需混淆的情况下创建具有特定技能的 Member 实例。

在 JDK 定义的库中,我们可以看到 BigInteger 的静态工厂方法 probablePrime()。


public static BigInteger probablePrime(int bitLength, Random rnd) {
    if (bitLength < 2)
        throw new ArithmeticException("bitLength < 2");

    return (bitLength < SMALL_PRIME_THRESHOLD ?
            smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
            largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));


比较 BigInteger 的普通构造函数和静态工厂方法 probablePrime(),后者更能描述返回的 BigInteger 的特性,即返回值为素数。


调用时不必每次都创建新实例

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


我们可以看到,Boolean 的 valueOf() 方法会预先缓存实例,然后将其返回。这种特性在需要频繁请求创建成本较高的对象的场景中可以显著提高性能,享元模式也属于类似的技术。


使用静态工厂方法以相同的方式返回相同对象的类被称为实例控制类。控制实例可以创建单例类或不可实例化类。此外,在不可变值类中,可以确保只有一个实例。

实例控制是享元模式的基础,枚举类型保证只有一个实例被创建。


示例

在 Minecraft 中,需要种植树木。如果每个树木对象都需要重新创建,则可能会导致内存溢出。

因此,可以将红色树木和绿色树木对象存储起来,然后只更改位置并返回。当然,颜色除了这两种颜色之外还可以更多,因此将树木存储在 Map 等数据结构中以供有效访问是有效的。


public class Tree {

    // 树木具有以下三个信息。
    private String color;
    private int x;
    private int y;

    // 只通过颜色创建构造函数。
    public Tree(String color) {
        this.color = color;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    // 种植树木时
    public void install(){
        System.out.println("x:"+x+" y:"+y+" 位置上有 "+color+" 色树木!");
    }
}

public class TreeFactory {
    // 使用 HashMap 数据结构来管理创建的树木。
    public static final Map treeMap = new HashMap<>();
    
   
    public static Tree getTree(String treeColor){
        // 在 Map 中查找输入的颜色是否有树木,如果有,则提供该对象。
        Tree tree = (Tree)treeMap.get(treeColor); 

       // 如果 Map 中还没有相同颜色的树木,则创建一个新对象并提供。
        if(tree == null){
            tree = new Tree(treeColor);
            treeMap.put(treeColor, tree);
            System.out.println("创建新的对象");
        }

        return tree;
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("请输入想要的颜色 :)");
        for(int i=0;i<10;i++){
            // 输入树木颜色
            String input = scanner.nextLine();
            // 从工厂获取一颗树
            Tree tree = (Tree)TreeFactory.getTree(input);
            // 设置树木的 x、y 位置
            tree.setX((int) (Math.random()*100));
            tree.setY((int) (Math.random()*100));
            // 种植树木
            tree.install();
        }
    }


与单例模式的区别

单例模式只能创建一个树类。因此,如果使用单例模式,则需要更改创建的唯一对象的颜色。也就是说,单例模式只允许存在一个对象,而不考虑类型。


使用案例

Java 的 String 常量池使用享元模式。


可以返回返回类型子类型的对象

如果您使用过 Arrays 实用程序类的 asList() 方法,那么您就会理解这个优点。


public static  List asList(T... a) [
    return new ArrayList<>(a);


asList() 方法使用 ArrayList 将值包装并返回,用户无需了解此实现细节。也就是说,可以灵活地选择返回对象的类,开发人员无需公开实现,因此可以保持 API 的简洁性。


关于 Java 接口的静态方法

在 Java 8 之前,接口无法声明静态方法,因此如果需要返回名为“Type”的接口的静态方法,则需要创建一个不可实例化的伴随类“Types”来定义该方法。


最典型的例子是 JCF 提供的 45 个实用程序实现,其中大多数实现都是通过静态工厂方法从单个伴随类 java.util.Collections 中获取的。尤其是这些实现中,有些实现不是公共的,只能通过静态工厂方法创建实例(当然无法继承这些实现)。


此外,由于没有公开这 45 个实现,因此 API 的规模也大大缩小。


// 接口和伴随类的示例


但是,从 Java 8 开始,可以在接口中直接添加静态方法,因此不再需要单独定义伴随类。


可以根据输入参数返回不同类的对象

不仅仅是返回子类型,还可以根据参数的值返回不同的子类型。例如,如果想根据分数返回不同的 MemberStatus,就可以像下面这样创建静态工厂方法,并在其中设置比较逻辑。


public enum MemberStatus {

    ADVANCED(80, 100),
    INTERMEDIATE(50, 79),
    BASIC(0, 49);

    private final int minScore;
    private final int maxScore;

    MemberStatus(int minScore, int maxScore) {
        this.minScore = minScore;
        this.maxScore = maxScore;
    }

    public static MemberStatus of(int score) {
        return Arrays.stream(values())
                .filter(decideMemberStatus(score))
                .findAny()
                .orElseThrow(() -> new NoSuchElementException("没有找到相应的 MemberStatus 对象。"));
    }

    private static Predicate decideMemberStatus(int score) {
        return element -> element.minScore <= score && element.maxScore >= score;
    }
}

@DisplayName("MemberStatus 测试")
class MemberStatusTest {

    @ParameterizedTest
    @CsvSource(value = {"0:BASIC", "30:BASIC", "50:INTERMEDIATE", "70:INTERMEDIATE", "80:ADVANCED", "100:ADVANCED"}, delimiter = ':')
    @DisplayName("根据分数返回不同的 MemberStatus。")
    void of(int input, MemberStatus expected) {
        assertThat(MemberStatus.of(input)).isEqualTo(expected);
    }


编写静态工厂方法时,不必存在要返回的对象的类

上面的句子中,对象的类指的是我们编写的类文件。

另外,Class 代表类加载器在加载类时分配到堆区域的 Class 对象。此 Class 对象包含我们编写的类的各种元数据。


package algorithm.dataStructure;

public abstract class StaticFactoryMethodType {

    public abstract void getName();

    public static StaticFactoryMethodType getNewInstance() {
        StaticFactoryMethodType temp = null;
        try {
            Class childClass = Class.forName("algorithm.dataStructure.StaticFactoryMethodTypeChild"); // 反射
            temp = (StaticFactoryMethodType) childClass.newInstance(); // 反射

        } catch (ClassNotFoundException e) {
           System.out.println("类不存在。");
        } catch (InstantiationException  e) {
            System.out.println("无法加载到内存中。");
        } catch (IllegalAccessException  e) {
            System.out.println("类文件访问错误。");
        }

        return temp;
    }


从上面的代码中,我们可以看到,通过接口实现的位置来创建 Class 对象,并使用反射技术来初始化实际的实现。此时,编写静态工厂方法的时刻StaticFactoryMethodTypeChild 类不必存在。


如果在使用静态工厂方法时,algorithm.dataStructure.StaticFactoryMethodTypeChild 路径中不存在实现,则会发生错误,但在编写静态工厂方法时不会出现问题,因此具有灵活性。


public interface Test {

    int sum(int a, int b);

    // Test 是一个接口,即使没有实现,在编写静态工厂方法时也不会出现问题。
    static Test create() {
        return null;
    }
}

public class Main {

    public static void main(String[] args) {
        Test test = Test.create();
        System.out.println(test.sum(1, 2)); // 发生 NPE
    }


即使不使用反射,也可以获得相同的灵活性。在 Test 的静态工厂方法 create() 中,即使没有实现,在编写时也不会出现问题。当然,在实际使用时会发生 NPE,因此需要稍后返回实现。


这种灵活性是创建服务提供者框架的基础,JDBC 就是一个典型的例子。JDBC 服务提供者框架的提供者是服务的实现,框架负责将这些实现提供给客户端,将客户端与实现分离(DIP)。


  • 服务提供者框架的组件
    • 服务接口
      • 定义实现的行为
      • JDBC 的 Connection
    • 提供者注册 API
      • 提供者注册实现
      • JDBC 的 DriverManager.registerDriver()
    • 服务访问 API
      • 当客户端获取服务的实例时使用,如果未指定条件,则返回默认实现或支持的实现,轮流返回。
      • 对应于静态工厂方法
      • JDBC 的 DriverManager.getConnection()
    • (可选)服务提供者接口
      • 如果没有,则在创建每个实现的实例时需要使用反射。
      • JDBC 的 Driver


服务提供者框架模式有多种变体,例如桥接模式、依赖注入框架等。


典型 JDBC 示例

Class.forName("oracle.jdbc.driver.OracleDriver"); 
Connection connection = null; 
connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root"); 


通常,JDBC 的编写方式如上所示。通过 Class.forName() 注册 Driver 实现之一 OracleDriver,然后通过 DriverManager.getConnection() 获取 OracleDriver 的 Connection 实现之一。


这里,Connection 是服务接口,DriverManager.getConnection() 是服务访问 API,Driver 是服务提供者接口。但是,提供者注册 API DriverManager.registerDriver() 没有使用。尽管如此,我们仍然可以使用 Class.forName() 注册 Driver 实现 OracleDriver。这是如何实现的?


Class.forName() 的工作原理

该方法将物理类文件名称作为参数传入,请求 JVM 加载该类。然后,类加载器将类的元数据存储到方法区域,并分配 Class 对象到堆区域。另外,当类加载完成后,静态字段和静态块将被初始化,此时将使用提供者注册 API。


public class OracleDriver implements Driver {

    static {
        defaultDriver = null;
        Timestamp timestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
        try {
            if (defaultDriver == null) {
                defaultDriver = new OracleDriver();
                DriverManager.registerDriver(defaultDriver); // 注册 OracleDriver
            }
        } catch (RuntimeException runtimeexception) {
        } catch (SQLException sqlexception) {
        }
    }

    ...


实际上,在 OracleDriver 中,我们可以看到在静态块中使用 DriverManager.registerDriver() 注册 Driver 实现 OracleDriver。


DriverManager 类分析

public class DriverManager {

    private DriverManager() {
    }

    private static final Map drivers = new ConcurrentHashMap();
    public static final String DEFAULT_DRIVER_NAME = "default";

    public static void registerDefaultPrivider(Driver d) {
        System.out.println("注册 Driver");
        registerDriver(DEFAULT_DRIVER_NAME, d);
    }

    public static void registerDriver(String name, Driver d) {
        drivers.put(name, d);
    }

    public static Connection getConnection() {
        return getConnection(DEFAULT_DRIVER_NAME);
    }

    public static Connection getConnection(String name) {
        Driver d = drivers.get(name);
        if (d == null) throw new IllegalArgumentException();
        return d.getConnection();
    }


DriverManager 类实际上要复杂得多,但仅从核心方面来看,它与上面类似。如上所述,通过在 OracleDriver 的静态块中调用 registerDriver() 注册 OracleDriver,用户可以调用 getConnection() 获取 Connection 的实现。


仔细观察用户访问 API getConnetion(),我们可以看到它是从 Driver 接口中获取 Connection 的。如果没有服务提供者接口 Driver,则需要使用 Class.forName() 等反射来返回想要的 Connection 实现。此时,Connection 实现不必在编写静态工厂方法时存在。


相反,我们使用 Driver 接口,动态地注册 Driver 的实现,然后可以轻松地获取与该 Driver 对应的 Connection 实现。


另外,我对 DriverManager 的 getConnection() 方法的实际 JDK 代码进行了分析,如果您不太感兴趣,可以跳过。


@CallerSensitive
public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));


首先调用公共静态方法 getConnection(),并将 url、Properties 和 CallerClass 作为参数传递给私有静态方法 getConnection()。此时,Reflection.getCallerClass() 负责获取调用该公共静态方法 getConnection() 的类的 Class 对象。如果 Car 类调用 getConnection(),则通过 Reflection.getCallerClass() 可以获得 Class 对象。


private static Connection getConnection(String url, java.util.Properties info, Class caller) throws SQLException {
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    SQLException reason = null;
    for(DriverInfo aDriver : registeredDrivers) {
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        }
    }

    if (reason != null)    {
        throw reason;
    }
    throw new SQLException("No suitable driver found for "+ url, "08001");


callerCL 是类加载器对象,由调用者或当前线程的类加载器创建。然后,从当前应用程序中注册的 Driver 列表 registeredDrivers 中逐个提取 aDriver。如果该 Driver 由 isDriverAllowed() 判定为 true,则使用该 Driver 获取 Connection 对象并返回。isDriverAllowed() 负责检查调用者中是否存在 aDriver。


JDBC 框架的优点

JDBC 框架的关键在于 Driver、Connection 接口及其实际实现的类完全分离提供。通过使用接口创建框架,然后根据框架创建各自的实现类,这具有非常灵活的优点。


因此,即使出现了其他 DBMS,该供应商也可以通过实现 Driver 和 Connection 接口来提供这些接口,从而使使用 Java 的开发人员能够使用与其他 DBMS 驱动程序相同的 API。


静态工厂方法的缺点

继承时需要公共或受保护的构造函数,如果只提供静态工厂方法,则无法创建子类


但是,这种限制更倾向于引导组合而不是继承,并且为了创建不可变类型,需要遵守这种限制,因此它反而可以成为优点。


静态工厂方法难以找到

由于它不像构造函数那样明确地显示在 API 说明中,因此开发人员需要编写良好的 API 文档,并使用广泛认可的约定为方法命名来缓解此问题。


静态工厂方法的命名方式

  • from
    • 接收一个参数并返回该类型的实例
    • Date date = Date.from(instant);
  • of
    • 接收多个参数并返回适合类型的实例
    • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf
    • from 和 of 的更详细版本
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance 或 getInstance
    • 返回指定的实例,但不保证是相同的实例。
    • StackWalker luke = StackWalker.getInstance(options);
  • create 或 newInstance
    • 与 instance 或 getInstance 相同,但保证每次都创建并返回新实例。
    • Object newArray = Array.newInstance(classObject, arraylen);
  • getType
    • 与 getInstance 相同,但在定义工厂方法的类不是要创建的类时使用。
    • FileStore fs = Files.getFileStore(path);
  • newType
    • 与 newInstance 相同,但在定义工厂方法的类不是要创建的类时使用。
    • BufferedReader br = Files.newBufferedReader(path);
  • type
    • getType 和 newType 的简化版本
    • List<Complaint> litany = Collections.list(legacyLitany);


总结

静态工厂方法和公共构造函数各有用途,请根据需要使用。


来源

제이온
제이온
제이온
제이온
[Java] 反射概念及使用方法 反射是 Java 程式執行時,能夠存取類別資訊並操作類別的 API。它允許在執行時建立類別實例,存取欄位和方法,但可能破壞封裝性並降低效能,因此應謹慎使用。在 Spring 框架等需要動態建立和管理物件的場合,它非常有用。

2024年4月25日

[有效 Java] 項目 3. 使用私有建構函式或枚舉類型來保證單例 本文章介紹了在 Java 中實現單例模式的三種方法(公用靜態成員、靜態工廠方法、枚舉類型),並說明了每種方法的優缺點以及序列化時的注意事項。考慮到反射攻擊的安全性以及程式碼簡潔性,文章建議使用枚舉類型作為最合適的方法。(資料來源:有效 Java, https://catsbi.oopy.io/d7f3a636-b613-453b-91c7-655d71fda2b1)

2024年4月27日

[Effective Java] 項目 6. 避免不必要的物件建立 這是一份關於在 Java 中減少不必要物件建立的指南。對於 String、Boolean 等不變物件,最好使用字面值;對於正規表示式,最好快取 Pattern 物件。此外,自動裝箱會導致效能下降,因此最好使用基本類型。有關更多資訊,請參閱「Effective Java」。

2024年4月28日

[非计算机专业,如何成为一名开发者] 14. 新手开发者常问的技术面试内容总结 本指南旨在为新手开发者提供技术面试准备指导。涵盖了面试中常见的概念,例如主内存区域、数据结构、关系型数据库 (RDBMS) 和 NoSQL、过程式编程和面向对象编程、重写和重载、页面替换算法、进程和线程、OSI 七层模型、TCP 和 UDP 等。
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자
투잡뛰는 개발 노동자

2024年4月3日

[Javascript] 物件的結構 (V8) JavaScript 的 Object 在 V8 引擎中根據狀態可以被優化為類似結構體的 Fast 模式或以雜湊表運作的 Dictionary 模式。Fast 模式是針對幾乎固定形式的鍵和值進行優化,速度很快,但當新增新鍵或刪除元素等操作時, 會轉換為 Dictionary 模式,速度會變慢。
곽경직
곽경직
곽경직
곽경직
곽경직

2024年3月18日

概念性數據模型 概念性數據模型是將實體分離並使用 ERD 表示實體間關係的過程。實體是獨立的資訊單位,屬性是 實體擁有的數據。識別碼用於唯一識別實體,關係表示實體間的交互作用。基數性表示實體間的數量關係,可選性表示數據的必需性。
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

2024年4月8日

用開源打造 AI 全棧 AI 生態系統中,新的開源 LLM(大型語言模型)模型不斷湧現。Mistral、Llama、phi-2 等具有強大性能和開放許可證的模型已公開, 為使用這些模型開發的各種工具也正在開發中。LangChain、LlamaIndex、Haystack 等 LLM 框架,Ollama、vLLM、KServe 等推論和 服務工具,LiteLLM、One API 等 LLM 代理解決方案,都讓 AI 開發
RevFactory
RevFactory
RevFactory
RevFactory

2024年2月5日

[并发] 原子操作:内存栅栏和内存顺序 这篇博文将解释在原子操作中如何考虑内存顺序,以及排序选项的重要性。 它将详细解释各种排序选项,例如 Relaxed、Acquire、Release、AcqRel 和 SecCst,以及每个选项的优缺点, 并提供使用示例代码。
곽경직
곽경직
곽경직
곽경직
곽경직

2024年4月12日

邏輯數據模型 邏輯數據模型是將概念數據模型轉換為關係數據庫範式的過程,根據映射規則,將 1:1、1:N、N:M 關係 轉換為關係數據庫中的表格,並通過正規化來確保數據完整性。通過 1NF、2NF、3NF 的正規化過程,對表格進行 優化,消除部分依賴和傳遞依賴。
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그
제이의 블로그

2024年4月9日