LucienXian's Blog


  • 首页

  • 归档

  • 标签

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

发表于 2019-01-26

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

概述

封装(encapsulation),良好的设计应该具备隐藏其内部数据和实现细节的特点,能够把它的API与它的实现清晰地隔离开,

访问控制

  1. 尽量使得每个类或者成员不被外界访问;
  • 如果类能被做成private的,它就应该被声明为private;
  • 如果一个private的类只有在某一个类的内部被用到,那就考虑使它成为唯一使用它的那个类的私有嵌套类;
  • 对于公有类的成员,当访问级别从包级私有变成protected级别后,会大大增加可访问性;
  1. 如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样可以保证任何使用超类实例的地方也都可以使用子类的实例;

  2. 实例域不能是公有的

如果域是非final的,或者指向一个可变对象的final引用。一旦域成为公有的,就会放弃了强制这个域不可变的能力。

注意长度非零的数组总是可变的,所以类具有公有的静态final数组域,或者返回这个域的访问方法都是错的。应该实现为:

1
2
3
4
5
6
7
8
9
10
private static final Thing[] PRIVATE_VALUES = {....};

// #1
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

// #2
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}

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

发表于 2019-01-26

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

不提供封装的类,如果不改变API,就无法改变它的数据表示法,也无法强加任何约束条件;

例如这种:

1
2
3
4
class Point {
public double x;
public double y;
}

如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误

Java的平台类库中有几个类违反了“公有类不应该直接暴露数据域”的告诫,比如java.awt包中的Point和Dimension类。

考虑实现Comparable接口

发表于 2019-01-26

考虑实现Comparable接口

概述

compareTo方法并没有在Object中声明,但它是Comparable接口中唯一的方法,不仅允许进行简单的同等性比较,而且还允许进行顺序比较。

建议

一旦类实现了Comparable接口,它可以跟许多泛型算法进行协作。在Java平台类库中的所有值类都实现了Comparable接口。

  1. 实现者必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x));
  2. 确保比较关系可传递:x.compareTo(y) > 0 && y.compareTo(z) > 0,那么x.compareTo(z)>0为true;
  3. x.compareTo(y) ==0 意味着所有z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))

另外,强烈建议:

1
(x.compareTo(y)==0) == (x.equals(y))

Java内存区域与内存溢出异常

发表于 2019-01-07

Java内存区域与内存溢出异常

概述

对于Java而言,垃圾回收技术和内存动态分配是它的一大特点,本文将介绍Java虚拟机内存的各个区域。

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它管理的内存划分为多个不同的数据区域——方法区(Method kArea)、堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stacj)、程序计数器(Program Counter Register)。前面两个是所有线程共享的,后者则是线程独立的。

程序计数器

每个线程都有自己的程序计数器,它可以看做是当前线程执行的字节码的行号指示器,通过改变这个计数器的值就可以选取下一条需要执行的字节码指令。

如果正在执行的是Native方法,这个计数器就是undefined。此内存区域没有规定任何outOfMemoryError情况。

Java虚拟机栈

同样是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法入口等。

对于局部变量而言,它存放着各种编译器可知的基本数据类型、对象引用和returnAddress类型。其中64位的long和double会占据2个局部变量空间。

如果线程请求的栈深度大于虚拟机允许的深度,会抛出stackOverflow异常;如果虚拟机动态扩展时无法申请到足够的内存,则会抛出outOfMemoryError。

本地方法栈

这个数据区的作用与虚拟机栈类似,只不过本地方法栈是为Native方法服务的。但由于虚拟机规范对此没有强烈的限制,因此例如Sun的HotSpot虚拟机直接把两个方法去合二为一。

Java堆

对于多数应用,heap是Java虚拟机管理内存中最大的一块,这是被所有线程共享的一块内存区域,存放的就是对象实例。

根据虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,当前主流的虚拟机都是按照可扩展来实现的——通过-Xmx和-Xms控制。

方法区

方法区同样是线程共享的,存放的内容包括已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虚拟机规范在对这个区域的限制比较宽松,除了跟堆一样可以处于物理上不连续的内存空间中,还允许不实现垃圾回收。

  • 运行时常量池

这是方法区的一部分,在Class文件中,除了有类的版本字段方法接口这些信息之外,还有一项信息是常量区。

运行时常量池还有另外一个重要特征——动态性。这个特性被利用的比较多的就是String#intern()方法。

直接内存

在JDK1.4中新引入了NIO类,引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存放在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

HotSpot虚拟机对象探秘

对象的创建

在Java的运行过程中,无时无刻都有对象被创建出来。那么在虚拟机中,这是一个怎么样的过程呢?

当虚拟机遇到一个new指令时:

  • 去常量池中检查是否能够定位到这个类的符号引用,并且检查这个类是否已被加载、解析和初始化过;
  • 在类加载检查通过后会去分配内存,这里有两种情况:
    • Java堆中内存规整,就会把所有用过的内存放一边,空闲的在另一边,中间使用指针作为分界点的指示器。每次分配内存的时候就移动指针——Bump the Pointer;
    • 非规整内存,则是维护一个空闲列表,记录哪些内存块可用;

Java堆是否规整与采用的垃圾回收器有关,CMS这种基于Mark-Sweep算法的收集器则是采用空闲列表。

另外,为了解决线程安全的问题,有两种解决方法:一是采用CAS配上失败重试的方式保证更新内存操作的原子性;二是将内存分配的操作按照线程划分在不同的空间中,则提取为线程分配缓存Thread Local Allocation Buffer——TLAB;可以通过-XX:+/-UseTLAB参数进行设定

  • 分配完内存后,会将内存空间中除对象头外都初始化为零值;
  • 接下来,虚拟机对对象进行必要的设置,将一些元数据信息,对象的哈希码,GC分代年龄等记录在Object Header中。
  • 最后,会执行init方法,进行对象初始化。

对象的内存布局

在HotSpot虚拟机中,对象的内存布局可以划分为三块区域——对象头、实例数据和对齐填充。

  1. 对象头

对象头包含两部分信息,一是存储对象自身的运行时数据,如对象的哈希码,GC分代年龄等,并且这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,即Mark Word。例如Mark Word的32bit空间中25bit存储对象哈希码,4bit存储对象分代年龄,2bit存储锁标志位,1bit固定为0;

二是类型指针,即对象指向它的类元数据的指针,这样就可以通过这个指针确定对象是哪个类的实例。

  1. 实例数据

这就是程序代码中定义的字段内容,包括从父类继承下来的。这部分的存储策略收到虚拟机分配策略参数和字段类型在Java源码中定义的顺序影响。

  1. 对齐填充

不是必然存在的,若对象实例数据部分不是8字节的整数倍则,需要对其填充进行补全。

对象的访问定位

创建对象是为了使用对象,Java程序是通过栈的reference数据来操作堆上对象的,因此这里有两种访问方式:

  • 通过句柄访问,Java堆会划分句柄池,reference存放的就是句柄的地址。这个方法的好处就是句柄地址稳定,对象被移动只会改变句柄的数据,reference本身不会被改变;
  • 直接访问,reference存放的是对象地址。这个方法的好处就是访问速度更快,这也是HotSpot采用的方式;

Two-phase Commit protocol

发表于 2018-12-22

2PC提交协议

概述

在分布式系统中,每个节点虽然可以知道自己的操作是否成功,但当一个事物跨越多个节点时,为了保证事务的ACID特性,系统需要引入一个协调者组件来管控所有节点的操作;

2 Phase Commit(简称2PC)协议是在分布式事务中,用于在多个节点达成一致性的协议。

协议内容

  • 角色

    • 一个协调者(coordinator,事务管理器)和多个参与者(participant,资源管理器)
  • 操作流程

    • Prepare阶段:
      • 协调者会向所有参与者发送prepare命令,然后开始等待参与者节点的响应;
      • 参与者会询问发起为止的所有实务操作,并将Undo和Redo信息持久化日志,此时会将事务在本地提交;
      • 向协调者应答commit同意或abort失败,协调者收到之后会将信息反馈到客户端;
    • commit阶段:在这个阶段,会根据不同的应答执行操作
      • 成功:当所有参与者节点都应答"同意";
        • 协调者向所有参与者节点发出"正式commit"的请求;
        • 参与者节点本地完成操作,并释放在整个事务期间占用的资源;
        • 参与者节点向协调者节点应答“完成”;
        • 协调者收到完成之后,完成事务;
      • 失败:存在参与节点在第一阶段响应“终止”,或者协调者等待超时
        • 协调者向所有参与者节点发出“回滚操作”的请求;
        • 参与者节点本地利用之前写入的Undo日志执行回滚,并释放相关资源;
        • 参与者向协调者反馈“回滚完成”;
        • 协调者收到所有信息之后,取消事务;

    两阶段完成之后,无论结果如何,协调者都必须要在此结束当前事务;

流程如下:

Coordinator

img
img

Participant

img
img

2PC的缺点

尽管2PC看起来能提供原子性的事务,但其也有局限:

  1. 同步阻塞:这是2PC最大的缺点,在执行过程中,节点都处于阻塞状态,即节点之间都在等待对方的相应消息。一旦协调者宕机,整个事务进程将会被阻塞;而参与者崩溃,则可能会导致协调者无法收到消息,执行保守的回滚政策;另外就是,一旦协调者发出了commit消息之后宕机,而唯一接收到这消息的参与同时崩溃,即便重新选出了协调者,那么这条事务的状态也是不确定的;
  2. 延迟:在prepare和commit两阶段,至少会产生两次RPC延迟,和三次持久化数据的延迟——prepare写日志+协调者状态持久化+commit写日志

总结

相比2PC,3PC能解决一些单点故障和阻塞的问题,但都无法彻底解决分布式的一致性问题,只有Paxos算法才是完整的一致性算法

there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. ——————Mike Burrows

Reference

  1. https://en.wikipedia.org/wiki/Two-phase_commit_protocol
  2. https://zhuanlan.zhihu.com/p/22594180
  3. https://www.hollischuang.com/archives/681

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

发表于 2018-12-16

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

概述

Singleton指仅仅被实例化一次的类。

实现方式

在jsk1.5之前,实现Singleton的两种方式都把构造函数声明为private:

  • 公有静态成员是一个final域
1
2
3
4
5
6
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...}

public void leaveTheBuilding{...}
}

但这种方法存在一个缺点,客户端可以通过反射来生成第二个实例,要避免这种弊端,可以在创建第二个对象的时候抛出异常

  • 公有成员是一个静态工厂方法
1
2
3
4
5
6
7
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance() {return INSTANCE; }

public void leaveTheBuilding{...}
}

jdk1.5发布之后,可以编写一个包含单个元素的枚举类型:

1
2
3
4
5
enum Elvis {
INSTANCE;

public void leaveTheBuilding{...}
}

清理过期的对象引用

发表于 2018-12-16

清理过期的对象引用

概述

在使用具备垃圾自动回收功能的编程语言时,可能会对不需要手动管理内存感到不可思议,以为不再需要考虑内存管理的事情。其实不然

例子

如果一个程序使用栈的时候先增长,后收缩,那么弹出来的对象将不会被当作垃圾回收,这是栈对对象的过期引用,虽然下标已经发生了改变,但对象应用却被无意识地保留了起来。

要解决这个问题,可以清空这些引用即可:

1
2
3
4
5
6
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

这样做还有一个好处,一旦被错误地引用,会抛出NullPointerException。

注意

  • 清空对象引用应该是一种例外,而不是一种规范行为;
  • 只要类是自己管理内存的,程序员应该警惕内存泄漏问题

内存泄漏来源

  1. 缓存

如果对象引用被放到缓存中,很可能被遗忘掉,以至于很长一段时间内都留在缓存中;

比如好的解决方法是用WeakHashMap代表缓存,但只有当所要的缓存项的生命周期是由key的外部引用而不是value决定时,才有用。tomcat的源码里,实现缓存时会用到WeakHashMap

  1. 监听器和其他回调

客户端在Api中注册了回调,却没有显式地取消回调。那么这时最好的解决方法是,将它们保存为WeakHashMap的key,即保存它们的弱引用。

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

发表于 2018-12-16

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

概述

静态工厂和构造函数都不能很好地扩展到大量的可选参数。

场景

考虑这样一个类——营养成分标签,其中有几个field时必需的,其他则是可选的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class NutritionFactsOne {
private int servingSize; //require
private int servings; //require
private int calorries;
private int fat;
private int sodium;
private int carbohydrate;

public NutritionFactsOne(int servingSize, int servings) {
this(servingSize, servings, 0);
}

public NutritionFactsOne(int servingSize, int servings, int calorries) {
this(servingSize, servings, calorries, 0);
}

public NutritionFactsOne(int servingSize, int servings, int calorries, int fat) {
this(servingSize, servings, calorries, fat, 0);
}

public NutritionFactsOne(int servingSize, int servings, int calorries, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calorries = calorries;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}

}

这样,我们创建一个实例时,就很不容易阅读:

1
NutritionFactsOne n = new NutritionFactsOne(240, 8, 100, 0, 35, 27);

当然,也可以考虑用java beans的模式,然后调用setter设置每个必要的参数,已经相关的可选参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class NutritionFactsTwo {
private int servingSize; //require
private int servings; //require
private int calorries;
private int fat;
private int sodium;
private int carbohydrate;

public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}

public void setServings(int servings) {
this.servings = servings;
}

public void setCalorries(int calorries) {
this.calorries = calorries;
}

public void setFat(int fat) {
this.fat = fat;
}

public void setSodium(int sodium) {
this.sodium = sodium;
}

public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}

紧接着

1
2
3
4
5
NutritionFactsTwo n = new NutritionFactsTwo();
n.setServingSize(240);
n.setServings(8);
n.setCalorries(100);
n.setFat(35);

但这个使用方式,在多线程的环境下有着致命的缺点,因为这有可能使得对象处于一个不一致的状态。

因此,我们可以使用builder的一种模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class NutritionFactsThree {
private final int servingSize; //require
private final int servings; //require
private final int calorries;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
private final int servingSize; //require
private final int servings; //require

private int calorries = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;


public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calorries(int calorries){
this.calorries = calorries;
return this;
}
public Builder fat(int fat){
this.fat = fat;
return this;
}
public Builder sodium(int sodium){
this.sodium = sodium;
return this;
}
public Builder carbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFactsThree build(){
return new NutritionFactsThree(this);
}
}
private NutritionFactsThree(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calorries = builder.calorries;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

builder的setter方法返回builder本身,这样就可以把调用链给连接起来:

1
2
3
4
5
NutritionFactsThree n = new NutritionFactsThree.Builder(240, 8)
.calorries(0)
.sodium(35)
.carbohydrate(27)
.build();

优点

  1. 客户端代码易于理解
  2. 可以对参数施加约束条件
  3. builder有多个可变参数
  4. builder相比javabeans更加安全

总结

如果类的构造器或者静态工厂中具有多个参数,设计类的时候,builder模式应该优先被选择,特别是大多数参数都optional的时候。

避免使用终结方法

发表于 2018-12-16

避免使用终结方法

概述

终结方法通常是不可预测的,危险的。

原因

  • 不能保证及时地被执行,因为JVM可能会延迟执行终结方法;
  • 在不同的JVM上对于执行终结方法的实现,即垃圾回收算法的实现是不同的;
  • 当一个程序终止时,某些已经无法访问对象上的终结方法可能根本没有执行;
  • System.gc System.runFinalization这个两个方法并不能保证终结方法被执行;
  • 如果未被捕获的异常在终结过程中被跑出来,那么异常可以被忽略,并且终结过程也会终止。这样的对象会处于破坏的状态;
  • 最后,使用终结方法会有严重的性能损失;

如何使用终结方法

如果类中的资源确实需要终止,那么应该提供一个显式的终结方法,例如IO中的close方法;

好处

  • 当对象的owner忘记使用显式的终止方法时,终结方法可以充当安全网,释放资源;

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

发表于 2018-12-16

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

概述

提供一个公有的静态工厂方法,返回类的一个实例

例子

1
2
3
public static Boolean valueOf(boolean b) {
return b ? Boolean.True : Boolean.False;
}

优势

  1. 拥有显式的名称;

用户可以清楚地知道自己需要那个构造函数构造的对象,例如BigInteger(int, int, Random)返回的是素数,但使用静态工厂方法BigInteger.probablePrime会更加明显。

  1. 不必每次调用的时候都创建一个新对象

静态工厂方法可以使用预先构建好的实例,或者将其缓存起来,重复利用。对象受控于静态工厂方法,可以确保它是一个单例。例如Boolean.valueOf这个方法从不创建新的实例。

  1. 可以返回原类型的任何子类型对象

这种灵活性带来的好处是,API可以返回对象,同时又不会使对象的类变成公有的,而且这个类还可以随着每次调用而发生变化。

  1. 在创建参数化类型实例的时候,代码将变得更加简洁

这里主要指的是类型推导。例如,原先的使用可能是:

1
Map<String, List<String>> map = new HashMap<String, List<String>>();

但假如我们有这样一个静态方法:

1
2
3
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}

这样,我们就可以代替上述啰嗦的使用方式:

1
Map<String, List<String>> map = HashMap.newInstance();

缺点

  1. 类如果不含公有或者protected的构造函数,就不能被子类化。
  2. 与其他的静态方法没有任何区别。
<i class="fa fa-angle-left"></i>1…121314…28<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces