系列文描述
hey,我是超级饭饭团,目前就职于广州某数字游戏公司,主攻java服务端。该系列是日常研发和个人学习中的笔记积累,目前定位是一周一次汇总分享(>▽<)。
Java系列
基于Java提供的Observer接口和Observable类实现观察者模式
作用描述:
在日常研发过程总会遇见业务A随着业务B发生相关变化的情况,这种情况便可以运用观察者模式,而对于观察者模式,Java已经为我们提供了已有的接口和类方便我们使用。
对于订阅者Java为我们提供了一个接口,JDK源码如下:
public interface Observer {
void update(Observable var1, Object var2);
}
可以看出,此处仅提供一个update方法用于接收通知者的通知做出相应改变。
在实际业务中,实现观察者模式的订阅者只需要实现该接口并实现update接口实现业务即可。
场景代码如下:
订阅者.png update.png再来看看Java提供了一个怎样的通知者,可以看到JDK源码如下:
public class Observable {
private boolean changed = false;
// 存放Observer的容器,本身是安全的,看了源码,内部实现的大部分函数都使用了synchronized
private Vector<Observer> obs = new Vector();
public Observable() {
}
// 为通知者添加订阅者的地方
public synchronized void addObserver(Observer var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
if (!this.obs.contains(var1)) {
this.obs.addElement(var1);
}
}
}
public synchronized void deleteObserver(Observer var1) {
this.obs.removeElement(var1);
}
public void notifyObservers() {
this.notifyObservers((Object)null);
}
public void notifyObservers(Object var1) {
Object[] var2;
synchronized(this) {
if (!this.changed) {
return;
}
var2 = this.obs.toArray();
this.clearChanged();
}
for(int var3 = var2.length - 1; var3 >= 0; --var3) {
((Observer)var2[var3]).update(this, var1);
}
}
public synchronized void deleteObservers() {
this.obs.removeAllElements();
}
protected synchronized void setChanged() {
this.changed = true;
}
protected synchronized void clearChanged() {
this.changed = false;
}
public synchronized boolean hasChanged() {
return this.changed;
}
public synchronized int countObservers() {
return this.obs.size();
}
}
首先我们可以从源码中看出Observable类使用Vector,Vector相比于ArrayList来说,它是线程安全的。其次,在多个函数上使用了synchronized关键字,这都是在为多线程考虑,避免出现在需要做出通知订阅者动作的时候因为数据紊乱出错的问题。
在实际业务中,直接继承该类即可,场景代码如下:
那么如何给监听者注册订阅者呢?场景代码如下:
注册操作.png而监听者通知订阅者的操作如下:
通知.png可以从上看出直接调用addObserver便可以给监听者注册订阅者,而在监听者发生变化的时候监听者调用setChange修改状态,之后调用notifyObservers通知,订阅者中的update实现便会被触发。
不允许改变的容器
作用描述:最近在研发中间件期间,由于业务需要,需要通过配置生成全局所有的容器,而该容器对外是不允许外界修改的,为了满足这种需求,使用了不可变集合,顾名思义,该种集合除了get操作,不允许外界增删改。
代码演示:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TestMain {
public static void main(String[] args) {
List<TestBean> originList = new ArrayList<>();
originList.add(TestBean.valueOf(1));
originList.add(TestBean.valueOf(2));
originList.add(TestBean.valueOf(3));
List<TestBean> unmodifiableList = Collections.unmodifiableList(originList);
for (TestBean t : unmodifiableList) {
System.out.println(t.getA());
}
unmodifiableList.add(TestBean.valueOf(4));
}
}
class TestBean {
private int a;
public static TestBean valueOf(int a) {
TestBean testBean = new TestBean();
testBean.a = a;
return testBean;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
代码很简单,先是构建了一个正常的List,后通过 List<TestBean> unmodifiableList = Collections.unmodifiableList(originList); 构建不可变容器,运行结果如下:
不可变容器.png由于不可变容器不允许对容器做add操作,于是报错了,直接查看jdk源码
不可变容器源码.png一句话总结:在研发中,如果需要使用到不可变容器来规避风险,那么可以尝试使用jdk提供的不可变容器,除了List外还有Map、Set等,具体可以自己查看。
枚举的反射
作用描述:最近遇见了将字符串反射为对应枚举的需求,发现java中枚举不允许通过newIntance反射出对象,因为内部自己禁止掉了,如
newIntance.png定位到throw的位置,此处做了限制。为了突破这个限制,我查看了Spring自身转换器的源码,终于找到了方法,果然多看源码是有用的。
代码演示如下:
枚举例子.png先给个枚举类型
int转enum.png此处是将int类型的数据转为对应的Enum,该int其实就是相当于你想转换的枚举在所在枚举里的位置。意思是如果int类型的数据是0的话,最后转换的枚举就是Currency。
string转enum.png此处是将str类型的数据转为对应的Enum,该str类型的数据指的是枚举的名字。意思是如果str类型的数据是Currency的话,最后转换的枚举就是Currency。
一句话总结:反射实例化枚举无法使用newInstance,请用以上两个例子,方便快捷。
关于类字面常量
描述:最近接触了比较多的反射,了解到类字面常量特殊的地方,故做笔录,同时也和大家分享下类字面常量特殊在哪里。所谓的类字面常量指的是A.class,这是java提供的生成对Class对象的引用。关于类的概念我们都很熟悉,关于类的使用JDK大致为我们做了三步操作,分别是
- 加载,这是由类加载器执行的,用白话来描述就是查找这个类的字节码,然后构建一个Class对象。
- 链接,在这个阶段会先校验类的字节码,并且为类对象的静态域分配好内存空间。
- 初始化,首先会先初始化类的超类(前提是有超类),以及执行静态初始化器和静态初始化块。
了解了三步还不够,我们还要知道jdk其实超级无敌懒的,对类的初始化这一步会延迟到对静态方法或者“非”常静态域进行首次引用时才执行。这里的常静态域指的是被static final修饰的常量,该常量不需要对应类初始化就可以被读取。为了更加清晰的了解到这个过程,可以看以下demo。
代码如下:
package classLoader;
import java.util.Random;
class Initable1 {
static final int staticFinal = 47;
static final int staticFinal2 = (int) (Math.random() * 1000);
static {
System.out.println("Initializing Initable1");
}
}
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.println("Initializing Initable2");
}
}
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random random = new Random(47);
public static void main(String[] args) throws ClassNotFoundException {
Class initable1 = Initable1.class;
System.out.println("After creating Initable1 ref");
System.out.println(Initable1.staticFinal);
System.out.println(Initable1.staticFinal2);
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("classLoader.Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
看具体运行结果前,看官先自己用草稿写下输出答案哈,这样比较有效果。
运行结果如下:
After creating INitable ref
47
Initializing Initable1
604
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
如果答案一样,那么可以直接忽略掉我接下来的解析了哈,因为分析很绕,主要是验证上面的理论!
正如答案中看到的那样,47 在 After creating INitable ref 后才输出,证明了 Class initable1 = Initable1.class 这一行代码运行的时候并没有立即初始化类,而 Initializing Initable1 在47后才输出也验证了另一个观点(47对应的Field staticFinal是被static final 修饰的常静态域),那就是常静态域不需要在类Initable1进行初始化就可以被读取。而在打印结果Initable1.staticFinal2(随机数)之前先打印出了Initable1静态代码块中的输出,意味着直到这一步才真正初始化了类Initable1,于是先执行了静态代码块再输出了Initable1.staticFinal2,同样,对Initable2.staticNonFinal的输出先打印了Initable2静态代码块中的Initializing Initable2也是同样的原因。而在最后使用Class.forName("classLoader.Initable3")取得Initable3类的引用的时候直接打印了代码块中的字符串可以看出,使用Class.forName取得类的引用的时候是立即初始化类的。
结尾说点什么
说好的一周一篇,上个周末沉迷docker的使用导致废了,然后最近又是每天都是差不多十二点下班,昨晚花了两个小时,今晚花了两个小时总算写好了,emmm,期待我基于反射开发的中间件哈哈哈哈(  ̄▽ ̄)((≧︶≦)
系列博客可以关注公众号:
公众号.jpg
网友评论