草庐IT

Effective Java 阅读日记 1

uwupu的学习日记 2023-04-18 原文

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

说明

在方法内部添加一个静态方法,用于获取一个对象,代替构造器的功能;

比如,在boolean包装Boolean类中,就有valueOf方法可以代替构造方法获得一个Boolean对象;

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

优势

  1. 静态方法有名字,可以指定一个功能作为方法名;

  2. 实现对象重用,优化程序运行;

    • 在对象使用结束后,可以将对象缓存起来,若下次调用可以再次使用;

    • 相对对象重用创建一个新的对象损耗可能会更大;

    • 在情况允许时,尽量多地使用对象重用,减少创建对象造成额外损耗;

    • 如Boolean类:Boolean类加载结束后,默认会创建两个Boolean对象,分别表示true和false,在使用静态工厂创建对象时,直接将代表true或false的对象返回,以节约内存使用和程序效率。

      public final class Boolean implements java.io.Serializable,
                                            Comparable<Boolean>
      {
      	//默认创建两个Boolean对象,用于表示TRUE和FALSE
          public static final Boolean TRUE = new Boolean(true);
          public static final Boolean FALSE = new Boolean(false);
      	// 包装了boolean类,这里存值
          private final boolean value;
      	// 构造方法新创建了一个Boolean对象
          public Boolean(boolean value) {
              this.value = value;
          }
      	//使用valueOf方法,直接返回Boolean类加载时创建的两个静态对象,无需再次创建对象。
          public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE);}
          public static Boolean valueOf(String s) {
              return parseBoolean(s) ? TRUE : FALSE;
          }
      }
      
  3. 依据不同的参数,可以返回任何子类的对象,也可以返回不同的对象;

    • 应用:
      • 静态方法可以返回对象,而无需将对象的类设为公有的;
      • 静态方法可以通过接口返回不同的对象EnumSet没有构造器,只能通过静态工厂创建对象,在OpenJDK实现中,EnmuSet的实现有两种类型:RegalarEumSetJumboEnumSet;当枚举元素数量等于小于64时,静态工厂方法返回RegalarEumSet对象;当枚举元素数量大于64时,静态工厂方法返回JumboEnumSet对象。(对于调用者,无需知道背后的实现原理,直接使用就好;对于EnumSet开发者,此做法用于代码优化。)
  4. 方法返回的对象所属的类,在编写静态工厂方法的类时可以不存在;

    • 如mysql和JDBC;

缺点

  1. 类如果不包含public的构造器,则不能被继承;

  2. 静态工厂方法需要程序员主动去寻找,而非构造方法可以直接使用;

    • 示例:
    class Apple {
        private String type;
        private String status;
    	//构造方法名与类名相同,可以直接使用
        public Apple(String type, String status) {
            this.type = type;
            this.status = status;
        }
        //静态工厂方法需要在API中寻找,没有构造方法方便
        public static Apple getNewApple(){return new Apple("redApple","fresh");}
    }
    
    • 一些惯例

      • from,从别的类型进行转换,只有一个参数;

      • of,将多个参数合并;

      //将多个参数合并到一起
      public Set<Apple> of(String ...colors){
          Set<Apple> apples = new HashSet<>();
          for (String s : colors) {
              apples.add(new Apple("Red"));
          }
          return apples;
      }
      
      • valueOf,也是类型转换;
      • createInstance或getInstance,通过参数获取一个对象,参数可以与成员变量不同;
        • createInstance或netInstance,保证每次返回一个新创建的实例;
        • getInstance一般用在单例模式。
      • getType(这里可以是getApple),与getInstance一致;
      • newType,与netInstance类似;
      • type,getType和newType的简化版。

2. 遇到多个构造器参数,可以考虑使用构建器(Builder)

说明

若一个类有多个参数,且对象使用构建器进行创建;

  • 有些参数有些时候不需要输入,但构造器中必须填入一个值;
  • JavaBeans模式,即一堆setter方法,这样可以解决上面的问题,但JavaBeans模式有严重的缺点,在构造过程中JavaBean可能处于不一致状态,即线程不安全

这个时候,就可以考虑使用建造者Builder模式

public class 建造者模式 {
    public static void main(String[] args) {
        Cat cat = new Cat.Builder("小黑")
                .age(12).color("White").build();
        System.out.println(cat);
    }
}
class Cat{
    private String name;
    private int age;
    private String color;
    private String owner;
    public static class Builder{
        //必要参数
        private String name;
        //可选参数
        private int age;
        private String color;
        private String owner;
        public Builder(String name) {this.name = name;}
        public Builder age(int val){age=val;return this;}
        public Builder color(String val){color=val;return this;}
        public Builder owner(String val){owner=val;return this;}
        public Cat build(){return new Cat(this);}
    }

    public Cat(Builder builder) {
        owner = builder.owner;
        color = builder.color;
        age = builder.age;
        name = builder.name;
    }
// toString 
}

Builder模拟了具有名字的可选参数,这样的客户端易于编写,易于阅读;

示例

代码

//这里先创建一个抽象类FriedRice
//然后分别创建两个类继承FriedRice,分别为FriedRiceWithHam和FriedRiceWithEgg
//fried rice 炒饭 可以添加 老干妈LaoGanMa、辣条LaTiao、再加一个鸡蛋Egg等
// ham 火腿   egg鸡蛋
//FriedRiceWithHam 火腿炒饭,可以有:大、中、小 三种  LARGE MEDIUM SMALL
//FriedRiceWithEgg 蛋炒饭,spicy辣度 可以选择:little微辣 general中辣 very特辣
//具体开发中不要使用中文,也不要使用拼音
//先整一个抽象类FriedRice
abstract class FriedRice{
    //额外要加的东西
    public enum Ingredient{老干妈,辣条,Egg}//实际开发不要使用中文
    private Set<Ingredient> ingredientSet;
    abstract static class Builder<T extends Builder<T>>{
        EnumSet<Ingredient> ingredients = EnumSet.noneOf(Ingredient.class);//默认没有配料
        public T addIngredient(Ingredient val){ingredients.add(val);return self();}//添加配料
        public abstract FriedRice build();
        protected abstract T self();
    }
    FriedRice(Builder<?> builder){
        ingredientSet = builder.ingredients.clone();
    }
}

//创建一个FriedRiceWithHam火腿炒饭
@ToString(callSuper = true)//是Lombok插件的注解,可以自动生成toString方法,文章主要讲解内容不包含这部分,忽略就好
class FriedRiceWithHam extends FriedRice{
    public enum Size{SMALL,MEDIUM,LARGE}
    private Size size;//大小
    public static class Builder extends FriedRice.Builder<Builder>{
        private Size size;
        public Builder(Size size){this.size = size;}
        @Override public FriedRice build() {return new FriedRiceWithHam(this);}
        @Override protected Builder self() {return this;}
    }
    FriedRiceWithHam(Builder builder) {
        super(builder);
        this.size = builder.size;
    }
}

//创建一个FriedRiceWithEgg鸡蛋炒饭
@ToString(callSuper = true)//是Lombok插件的注解,可以自动生成toString方法,文章主要讲解内容不包含这部分,忽略就好
class FriedRiceWithEgg extends FriedRice{
    public enum Spicy{LITTLE,GENERAL,VERY}
    private Spicy spicy;
    public static class Builder extends FriedRice.Builder<Builder>{
        private Spicy spicy;
        public Builder(Spicy spicy){this.spicy = spicy;}
        @Override public FriedRice build() {return new FriedRiceWithEgg(this);}
        @Override protected Builder self() {return this;}
    }
    FriedRiceWithEgg(Builder builder) {
        super(builder);
        spicy = builder.spicy;
    }
}

使用

public class Builder模式也适用于类层次结构 {
    public static void main(String[] args) {
        //创建一个鸡蛋炒饭,中辣,添加老干妈
        FriedRice friedRiceWithEgg = new FriedRiceWithEgg.Builder(FriedRiceWithEgg.Spicy.GENERAL)
                .addIngredient(FriedRice.Ingredient.老干妈).build();
        //创建一个火腿炒饭,大份,添加鸡蛋
        FriedRice friedRiceWithHam = new FriedRiceWithHam.Builder(FriedRiceWithHam.Size.LARGE)
                .addIngredient(FriedRice.Ingredient.Egg).build();
    }
}

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

Singleton,即单例模式;对于一个类,只会被实例化一次,后续通过静态方法获取对象也只能获取到这一个对象,不会再次创建新的对象。

创建一个Singleton,有两种方式

私有构造器

将构造器私有化,然后通过getInstance方法创建并获取对象。

发展

默认情况下,可以通过以下方式实现单例模式。
//Chopsticks n.筷子
//这里假定筷子只能有一根
//这里创建一个单例对象
class Chopstick{
    private static final Chopstick INSTANCE = new Chopstick();//类加载后,自动创建一个Chopstick对象,
    private Chopstick(){}//构造器私有化,禁止二次创建
    public static Chopstick getInstance(){return INSTANCE;}//获取实例
}
但是,这个单例是可以通过反射进行破坏;
public static void main(String[] args) throws Exception {
    Chopstick instance = Chopstick.getInstance();//第一个实例对象
    //第二个实例对象
    Class<?> aClass = Class.forName("com.yn.study.chapter1.Chopstick");//获取Class对象
    Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();//获取Constru对象
    declaredConstructor.setAccessible(true);//跳过private检查
    Chopstick chopstick = (Chopstick) declaredConstructor.newInstance();//创建实例对象

    System.out.println(instance);
    System.out.println(chopstick);
    /**输出结果如下:
         * com.yn.study.chapter1.Chopstick@7f31245a
         * com.yn.study.chapter1.Chopstick@6d6f6e28
         * 表示这两个对象不是同一个对象
         */
}
所以,可以在构造方法里面添加判断,让第二次创建过程抛出错误来解决破坏;
class Chopstick{
    private static final Chopstick INSTANCE = new Chopstick();//类加载后,自动创建一个Chopstick对象,
    private Chopstick() {if (INSTANCE!=null)throw new Error("请不要二次创建对象");}//构造器私有化,禁止二次创建
    public static Chopstick getInstance(){return INSTANCE;}//获取实例
}
如果要使对象变得可序列化,必须声明readResolve方法

如果要使对象变得可序列化,仅仅在声明中加上implements Serializable是不够的,为了维护Singleton,必须声明所有实例域是transient(瞬时)的,并声明readResolve方法;

否则,每当反序列化一个对象,都会创建一个新的对象;

public static void main(String[] args) throws Exception {
    //单例破解方案——序列化:将对象存储于文件中,然后从文件中读取

    //创建一个单例对象
    Chopstick chopstick1 = Chopstick.getInstance();

    //将对象写入文件
    File file = new File("Chopstick.dat");
    FileOutputStream os = new FileOutputStream(file);
    ObjectOutputStream oos = new ObjectOutputStream(os);
    oos.writeObject(chopstick1);
    oos.close();os.close();
    //将对象从文件中读取
    FileInputStream is = new FileInputStream(file);
    ObjectInputStream ois = new ObjectInputStream(is);
    Chopstick chopstick2 = (Chopstick) ois.readObject();//第二个实例化对象
    System.out.println(chopstick1);
    System.out.println(chopstick2);
    /**
     * 输出结果
     * com.yn.study.chapter1.Chopstick@2503dbd3
     * com.yn.study.chapter1.Chopstick@7ef20235
     * 表示这两个对象不是一个对象
     */
}

声明readResolve方法

private Object readResolve(){return INSTANCE;}

这样,上面的结果获得的将是同一个对象。

com.yn.study.chapter1.Chopstick@2503dbd3
com.yn.study.chapter1.Chopstick@2503dbd3

使用

//Chopsticks n.筷子
//这里假定筷子只能有一根
//这里创建一个单例对象
class Chopstick implements Serializable {
    private static final Chopstick INSTANCE = new Chopstick();//类加载后,自动创建一个Chopstick对象,
    private Chopstick() {if (INSTANCE!=null)throw new Error("请不要二次创建对象");}//构造器私有化,禁止二次创建
    public static Chopstick getInstance(){return INSTANCE;}//获取实例
    private Object readResolve(){return INSTANCE;}//写readResolve方法,防止反序列化破坏单例
}

枚举类

枚举本就是一个单例对象,而且不可破坏。

enum ChopstickPlus{
    INSTANCE;
    ChopstickPlus getInstance(){return INSTANCE;}
}

4. 通过私有构造器,使得类不可实例化

有些类只包含静态方法或静态域,这样的类不希望会被实例化,因为这些类被实例化是没有意义的;

这里我表示疑惑:应该一般情况下没有人会去尝试实例化一个只有静态方法的类,嗯..但是...,书上说有一些时候会无意识的初始化该类??下面继续记笔记。

对于没有特别声明构造器的类,其构造器默认是public的,

  • 这里可以通过将构造器私有化,来避免不必要的实例化。

  • 同样,为避免通过反射创建对象,可以在构造方法里添加抛出错误,防止类实例化。

//EasyMath 一个简单的,无意义的,仅用于学习的,计算类
class EasyMath{
    public static long sum(long a,long b){return a+b;}//一个求和的静态方法
    //不希望不必要的工具类实例化
    private EasyMath(){throw new AssertionError();}
}

但这样有个缺点:这个类不能有父类。

5. 优先考虑依赖注入引用资源

这里..就只写个标题吧。。

详见Effective Java 第三版 P16页。

有关Effective Java 阅读日记 1的更多相关文章

  1. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  2. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  3. ruby-on-rails - 在 irb 中阅读文档 - 2

    我怀念ipython的一件事是它有一个?为特定功能挖掘文档的运算符。我知道ruby​​有一个类似的命令行工具,但是我在irb中调用它非常不方便。ruby/irb有类似的东西吗? 最佳答案 Pry是IPython的Ruby版本,它支持?命令来查找有关方法的文档,但语法略有不同:pry(main)>?File.dirnameFrom:file.cinRubyCore(CMethod):Numberoflines:6visibility:publicsignature:dirname()Returnsallcomponentsofthef

  4. ruby - 如何将 Vim 中的 "expand"文本转换成一种易于阅读的方式? - 2

    我经常使用嵌套数据结构,很多时候我必须从控制台手动分析它们。问题是它们全部打印在一行中。是否有一种简单的方法可以根据{,[,],}和逗号重新构造数据结构的显示,使其看起来像Ruby的pretty_print输出? 最佳答案 :%s/\([{,]\)/\1\r/gggVG=:setft=ruby呜呜呜 关于ruby-如何将Vim中的"expand"文本转换成一种易于阅读的方式?,我们在StackOverflow上找到一个类似的问题: https://stacko

  5. 基于SpringBoot的线上日志阅读器 - 2

    软件特点部署后能通过浏览器查看线上日志。支持Linux、Windows服务器。采用随机读取的方式,支持大文件的读取。支持实时打印新增的日志(类终端)。支持日志搜索。使用手册基本页面配置路径配置日志所在的目录,配置后按回车键生效,下拉框选择日志名称。选择日志后点击生效,即可加载日志。windows路径E:\java\project\log-view\logslinux路径/usr/local/XX历史模式历史模式下,不会读取新增的日志。针对历史文件可以分页读取,配置分页大小、跳转。历史模式下,支持根据关键词搜索。目前搜索引擎使用的是jdk自带类库,搜索速度相对较低,优点是比较简单。2G日志全文搜

  6. ruby - 在使用 ruby​​ 中的 "mail"gem 阅读电子邮件时需要帮助 - 2

    我正在使用Watir进行自动化,它会创建一封我需要检查的电子邮件。有人指出电子邮件gem是执行此操作的最简单方法。我添加了以下代码,并且能够从我的收件箱中收到第一封电子邮件。require'mail'require'openssl'Mail.defaultsdoretriever_method:pop3,:address=>"email.someemail.com",:port=>995,:user_name=>'domain/username',:password=>'pwd',:enable_ssl=>trueendputsMail.first我是这个论坛的新手,有以下问题:如何获

  7. ruby - 阅读用户输入时如何避免回显换行符? - 2

    我正在用Ruby编写类似curses的程序,我正在使用stty和ansi转义字符来实现我想要的。当我想获得用户输入时,我的问题就出现了。像许多基于控制台的程序一样,我想从终端底部获取用户输入。因此,我将光标放在屏幕底部并调用Readline.readline(或任何获取用户输入的方法)。像往常一样,它会读取所有内容,直到我按下回车键,并打印一个换行符。由于光标位于终端的最后一行,它会滚动一行,这会弄乱屏幕。我怎样才能避免这种情况?我试图使用stty来停止回显换行符,但我没有成功。也许可以使用stty来阻止终端滚动?当然,我可以编写自己的方法来通过一次读取一个字符(并捕获“返回”)来捕获

  8. ruby-on-rails - 阅读 Rails session secret 的最佳方式是什么? - 2

    我想以编程方式访问Railssessionsecret(我正在使用它来生成登录token)。这是我想出的:ActionController::Base.session.first[:secret]这将返回sessionsecret。但是,每次调用ActionController::Base.session时,它都会向数组中添加另一个条目,因此您最终会得到如下内容:[{:session_key=>"_new_app_session",:secret=>"totally-secret-you-guys"},{},{},{},{},{},{},{},{},{},{},{},{}]我觉得这不太

  9. ruby - 将文本粘贴到 IRB 中非常慢。阅读线问题? - 2

    当我将以下文本粘贴到在ruby​​-enterprise-2011.03下运行的IRB或PRY时,需要13秒。#Loremipsumdolorsitamet,consecteturadipisicingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.在同一台计算机上运行irb和其他ruby​​安装时,粘贴并不慢。jruby-1.5.6jruby-1.6.3ruby-1.8.6-p420ruby-1.8.7-p352ruby-1.9.1-p431ruby-1.9.2-p290ruby-1.9.3-preview1o

  10. ruby-on-rails - 你如何阅读现有的 Rails 项目? - 2

    当您开始处理现有的Rails项目时,您采取了哪些步骤来理解代码?你从哪里开始?在深入了解Controller、模型、助手和View之前,您使用什么来获得高级View?您是否有任何特定的技术、技巧或工具可以帮助加快该过程?请不要回复“学习Rails和Ruby”(就像问这个问题的lastguy的回复之一——他的问题也没有得到太多回复,所以我想我会再问一次并提示多一点)。我对自己的代码很满意。它正在对其他人进行分类,这让我很头疼,需要很长时间才能理解。 最佳答案 看看模型。如果应用程序编写得很好,这应该为您提供其域模型的图片,这是有趣的逻

随机推荐