改善java程序的151个建议 - 读书笔记

2023-11-18

目录

第1章 java开发中通用的方法和准则

1.字母“l”和数字1、字母“o”和数字0易混淆

应该避免这种弄易混淆的组合一起出现,如果必须出现,“l”务必大写,“L”;字母“o”或者“O”加注释。

public void suggest_1(){
		long i = 1l;
		System.out.println(i+i);//会打印出22吗?

		String str1 = "hell0";
		String str2 = "hello";

		System.out.println(str1.equals(str2));
	}
2
false

Process finished with exit code 0

3.三元操作符的类型务必一致

三元操作符是if-else的简化写法

public void suggest_3() {
        Integer i = 80;
        String s1 = String.valueOf(i < 100 ? 90 : 100);
        String s2 = String.valueOf(i < 100 ? 90 : 100.0);
        System.out.println(s1.equals(s2));
        System.out.println(s1);
        System.out.println(s2);
    }
false
90
90.0

Process finished with exit code 0

7.警惕自增陷阱

自增指的是i++、++i.这两种写法都可以实现i自增
i++先赋值后++、++i先自增后赋值
掉进陷阱的代码

public void suggest_4() {
		int count = 0;
		for (int i=0;i<10;i++){
			count = count ++;
		}
		System.out.println(count);//会打印出10?
	}
0

Process finished with exit code 0

为什么会打印出0呢?因为count++是有返回值的,原理如下:

	int temp = i;
  i = i + 1;
  return temp; 

11.好习惯是显示声明UID

为什么要加UID?
持久化 - 持久化意味着对象的“生存时间”不依赖于程序是否在执行。
远程调用,其他机器上的对象好像在本地机器一样,就是使用了序列化技术持久化其他机器的对象。

UID是怎么计算出来的?
SerialVersionUID也叫流标识符,即类的版本定义,他可以显示声明也可以隐式声明。隐式声明时编译期在编译的时候帮我声明。生成的依据是包名、类名、继承关系、参数、返回值等等,极其复杂,计算出一个唯一值。

有什么用?
以远程调用为例。一个远程调用的参数bean在api中定义,consumer和provider引用不同的api版本,consumer端发送UID=1,provider在反序列化这个bean的时候发现和自己引用的bean的UID不相等,因此抛出一个InvalidClassException。

显示声明UID有什么好处?
可实现反序列化版本向上兼容。提高代码健壮性
以远程调用为例。api版本api_v=1.0,某天在bean中增加了一个属性age,版本升级为api_v=2.0。但是bean显式声明了UID,所以在provider反序列化是还是可以序列化成功的,只是反序列化出来没有age属性。

几个点

  • 显示声明的UID应设置为private,避免继承而流放到子类。
  • 数组无法显示声明序列化ID,java的序列化也不会比较数组。

12.Serializable类中的final成员变量的赋值

api中的bean包含一个final属性name=“andy”。

public class Person implements Serializable{      
     private static final long serialVersionUID = 91282334L;   
     public  final String name="andy";

consumer单方面升级api改动name=“fiona”,那么provider反序列化这个bean的时候name反序列化成什么值?

答案是反序列化成fiona。
这个case,name在反序列化时被重新计算,但并不是所有的情况都会重新计算。

不重新计算的case1:通过构造方法赋值

public class Person implements Serializable{      
     private static final long serialVersionUID = 91282334L;  
     public  final String name;    
     //构造函数为不变量赋值  
     public Person(){  
         name="andy";  
    }  
} 

provider执行反序列化的时候,根据类描述信息发现name是final类型,需要重新计算,然后找到Person类,发现name根本没有被赋值,所以JVM直接取网络传输过来的name值。

不重新计算的case2:通过方法赋值

public class Person implements Serializable{      
     private static final long serialVersionUID = 91282334L;  
     //通过方法返回值为final变量赋值  
     public  final String name = initName();  
     //初始化方法名  
     public String initName(){  
           return "andy";  
     }  
} 

provider执行反序列化的时候,根据类描述信息发现name是final类型,需要重新计算,然后找到Person类,发现name通过方法赋值,所以JVM直接取网络传输过来的name值。
可能有人会问,为什么不执行initName()来重新计算?
答案是,当然不能执行initName(),因为initName()里边除了做name赋值可能还有其他事情要做。

反序列化原理:
序列化后的对象包含以下信息:

  • 包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现
  • 非瞬态(transient关键字)和非静态(static关键字)的实例变量值

注意,这里的值如果是一个基本类型,好说,就是一个简单值保存下来;如果是复杂对象,也简单,连该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列化异常),也就是说递归到最后,其实还是基本数据类型的保存。

总结一下,final不会被重新计算的case:

  • 通过构造方法为final变量赋值
  • 通过方法返回值为fianl变量赋值
  • final修饰的属性不是基本类型

final会被重新计算的case:

  • 8个基本类型,以及数组、字符串(字符串情况很复杂,不通过new关键字生成String对象的情况下,final变量的赋值与基本类型相同),但是不能方法赋值。

14.影响序列化和反序列化

在一些保密场景,我们不希望一些字段暴露出去。
可以用readObject(ObjectInputStream inputStream)、writeObject(ObjectOutputStream outputStream)来影响序列化和反序列化。

16.易变业务使用脚本语言编写

避免不断的发布java变更
脚本语言很强发

17.instanceof的使用

public void suggest_17() {

		//System.out.println('A' instanceof  Character);//编译异常,instanceof左右都不能是基本类型
		System.out.println((String)null instanceof  String);//null强转之后也还是null
		System.out.println(new GenericClass<String>().isDateInstance(""));
	}
	class GenericClass<T>{
		public boolean isDateInstance(T t){
			return t instanceof Date;
		}
	}
false
false

Process finished with exit code 0

泛型是为编码服务的,在编译成字节码时,T已经是Object类型。

20.发布系统禁止使用文件替换的方式

public void suggest_20() {
		System.out.println(Constant.MAX_AGE);
	}
	
class Constant{
	public final static int MAX_AGE = 150;
}

开发人员直接修改Constant.MAX_AGE=180替换线上Constant.class文件,然后重启服务器。但是发现suggest_20中输出的还是150,原因是

  • fianl修饰基本类型和String时,编译器认为它是稳定态,所以在编译是把值写在了字节码中。避免运行时引用,提高效率。在上边的例子中,suggest_20中的Constant.MAX_AGE在编译是就被置换为150而不是引用。
  • final修饰类,编译器认为是不稳定态,在编译期建立的是引用关系。

所以发布时,整体WAR包发布才是万全之策。

第2章 基本类型

21.用i%2 == 0判断奇偶

public void suggest_21() {
		System.out.println("-1 是" + (-1 % 2 == 1? "奇数" :"偶数"));
	}
-1 是偶数

Process finished with exit code 0

可见,用i%2 == 1判断奇偶是有问题的。%取余的原理:

public static int remainder(int dividend,int divisor) {
		return dividend - dividend / divisor * divisor;
	}

22.用整数处理货币

public void suggest_22() {
		System.out.println(10.00 - 9.60);//会输出0.4吗?
	}
0.40000000000000036

Process finished with exit code 0

原因是:在计算机中,浮点数可能是不准确的,它只能无限接近准确值。因为0.4无法用二进制准确表示。在二进制世界中,0.4是无限循环的小数,就像十进制中的1/3。
表示货币的方法:

  • 使用BigDecimal
  • 使用整型:吧参与运算的值扩大100倍,转为整型,展现时再缩小100倍。

23.不要让类型默认转换

public void suggest_23() {
		int LIGHT_SPEED = 30 * 10000 * 1000;//光速
		long dis = LIGHT_SPEED * 60 * 8;
		System.out.println("太阳与地球的距离是:"+dis);
	}
太阳与地球的距离是:-2028888064

Process finished with exit code 0

在程序第4行,已经考虑到会超过int表示范围,改用long,但是还是得到了错误的结果。这是因为这里是把计算后的值转成了long类型。

第4行可以这样写

long dis = LIGHT_SPEED * 60L * 8;

integer越界了为什么得到负值?因为越界了就会从头开始。

24.边界检查

场景:某个商品搞促销,每个人最多可以购买2000件。

public void suggest_24() {
		int from_web = 2147483647;//本次购买的数量 Integer.MAX_VALUE = 2147483647
		int from_db = 800;//已经购买的数量
		int MAX_COUNT = 2000;//阈值

		if(from_web + from_db <= MAX_COUNT){
			System.out.println("购买成功");
		}else {
			System.out.println("购买失败");
		}
	}
购买成功

Process finished with exit code 0

前端传过来Integer.MAX_VALUE,Integer.MAX_VALUE+800超过Integer最大表示数其结果是负数,所以这里校验失败了。
Integer的取值范围是-2^31~ 2^31-1

public void integer_range() {
		System.out.println(Integer.MAX_VALUE);
		System.out.println(Integer.MIN_VALUE);
		System.out.println(Integer.MAX_VALUE + 1);
		System.out.println(Integer.MAX_VALUE + 2);
	}
2147483647
-2147483648
-2147483648
-2147483647

Process finished with exit code 0

25.不要让四舍五入亏了一方

java支持7中四舍五入模式。

public void suggest_25() {
		BigDecimal d = new BigDecimal(1);

		BigDecimal r1 = new BigDecimal(10.115);

		BigDecimal r2 = new BigDecimal(10.125);

		BigDecimal i1 = d.multiply(r1).setScale(2,BigDecimal.ROUND_HALF_EVEN);
		System.out.println("3个月的利息是"+i1);
		BigDecimal i2 = d.multiply(r2).setScale(2,BigDecimal.ROUND_HALF_EVEN);
		System.out.println("3个月的利息是"+i2);
	}
3个月的利息是10.12
3个月的利息是10.12

Process finished with exit code 0

七种舍入模式:

  • ROUND_UP:原理零方向舍入
  • ROUND_DOWN:趋向零方向舍入
  • ROUND_CEILINE:正无穷方向舍入。math.round使用的模式。
  • ROUND_FLOOR:负无穷方向舍入
  • HALF_UP:经典四舍五入(5近)
  • HALF_DOWN:5舍
  • HALF_EVEN:银行家算法。四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇应进一。

29.优先使用基本类型

30.不要随便设置随机种子

public void suggest_30() {
		Random r = new Random(100);//设置随即种子=100

		System.out.println(r.nextInt());
		System.out.println(r.nextInt());
		System.out.println(r.nextInt());
	}
-1193959466
-1139614796
837415749

Process finished with exit code 0

设置随即种子后,每次运行都返回这三个值。原因在于:

  • 种子不同产生不同的随机数
  • 种子相同,产生相同的随即数

Random r = new Random(100);//种子固定,产生相同的随机数
Random r = new Random();//默认的构造方法,种子=System.nanoTime距离一个时间的纳秒数。产生不同的随机数。

第3章 类、对象及方法

33.不要复写静态方法

因为静态方法不能复写,只能隐藏。
静态方法的两种引用方式

  • 类名访问。
  • 实例访问。JVM通过实例的表面类型查找静态方法的入口。(不要用这种方式引用静态方法)

34.简化构造函数

构造函数应该简化再简化,应该达到“一眼洞穿”的境界

35.避免在构造函数中初始化其他类


这段代码会报异常,StackOverFlowError。因为初始化子类之前要先初始化父类,这里死循环了。

36.使用构造代码块精简程序


等价于

可见,构造代码块的执行时机是在构造方法的开始。

这样的case:如果一个构造方法A用this.b()调用构造方法B,代码块在构造方法B不执行。遇到this不执行代码块。

构造代码块使用场景:

  • 每个构造方法都需要的初始化实例变量
  • 初始化实例环境

38.使用静态内部类提高封装性

静态内部类和普通内部类有什么区别:

  • 静态内部类不持有外部类的引用
  • 静态内部类不依赖外部类
  • 静态内部类可以声明static的方法和变量

39.使用匿名类的构造函数

public void suggest_39() {
		List list = new ArrayList(){{}};
	}

这是使用了匿名类,和空构造代码块,等价于

class SubList extends ArrayList{
		{

		}
	}
	list = new SubList();

现在,也能看懂下面这段代码了吧

public void suggest_39() {
		List list = new ArrayList(){{}{}{}{}{}};
	}

注意:有参数的匿名类先调用父类的同参构造再执行自己的构造代码块。这一点与普通继承是不一样的,普通继承是下调用父类的无参构造。

41.用内部类/匿名内部类实现多重继承

42.让工具类不可实例化

两个办法:

  • 将构造方法声明为private。java反射修改访问权限就控制不住了。
  • 在构造方法中抛出异常

43.避免对象浅拷贝

实现cloneable接口就具备了copy能力。

这就是浅copy。也是cloneable默认的拷贝方式,拷贝规则如下:

  • 基本类型copy值
  • 引用类型copy引用
  • String类型copy引用,但是修改是会生成新的字符串。可认为是基本类型。

推荐使用序列化的方式实现对象的深拷贝:
使用Apache下的commons中的工具类SerializationUtils

45.复写equals是不要识别不出自己

46.复写equals要考虑null

47.复写equals用getClass作类型判断不要使用instanceof

第4章 字符串

52.直接量赋值String是在常量池中创建

52.replaceAll传递的第一个参数是正则表达式。

53.注意字符串的位置。

public void suggest_53() {
		String str1 = 1 + 2 + "apple";
		String str2 = "apple" + 1 + 2;

		System.out.println(str1);
		System.out.println(str2);
	}
3apple
apple12

Process finished with exit code 0

54字符串拼接。

三种方式:+、concat、StringBuilder.
性能从高到底:StringBuilder、concat、+

  • 的实现原理:
Str = new StringBuilder(str).append("c").toString();

耗时在toString();

concat的实现原理:

还是在最后的new String()

57.推荐在复杂字符串中使用正则表达式

第5章 数组、集合

62警惕数组的浅拷贝。

Arrays.copyOf是shallow clone

64用treeSet实现去重后排序很不错。

65原始类型数组不能作为asList的输入参数。

public void suggest_65() {
		int[] date = {1,2,3,4,5};
		List list = Arrays.asList(date);

		System.out.println("元素类型"+list.get(0).getClass());
		System.out.println(date.equals(list.get(0)));
	}
元素类型class [I
true

Process finished with exit code 0

我们期望通过List list = Arrays.asList(date);得到一个长度为5的List,得到的却是一个长度为1的list。并且第一个元素是一个数组。原因是Arrays.asList的原理:

 @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

因为asList接收多个可泛型化的参数,数组是对象是可泛型化的。上边的程序改为Integer数组就可以正常转换了,如下:

public void suggest_65() {
		Integer[] date = {1,2,3,4,5};
		List list = Arrays.asList(date);

		System.out.println("元素类型"+list.get(0).getClass());
		System.out.println(date.equals(list.get(0)));
	}
元素类型class java.lang.Integer
false

Process finished with exit code 0
// An highlighted block
var foo = 'bar';

为什么元素类型class [I?
因为java没有Array的定义,Array是通过java.lang.reflect得到的,所以JVM无法输出Array的类型。

asList()还有一个需要注意的地方:asList()返回一个长度不可变的list。也就是说只有在只读场景才用的到asList()。
因为asList()返回的不是java.lang.ArrayList,而是java.util.Arrays.ArrayList。

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
   }

元素数组是final类型看到没,而且,这个内部类也没没有实现add()、remove等修改元素个数的方法。

67不同的列表选择不同的遍历方法。

ArrayList 基于数组实现
LinkedList 基于链表实现

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

ArrayList实现了RandomAccess,元素之间无关联,可通过下标随即访问。
LinkedList 双向链表实现,元素之间有关联,不能通过下标快速访问。

list的访问方式

  • 下标访问。for(int i=0;i<len;i++)
  • Iterator访问。(forEach也是用的Iterator)。
    Iterator做遍历时,元素之间需要有序,如果无需,则需要建立顺序。
. ArrayList LinkedList
下标访问 推荐
Iterator 推荐

LinkedList用下标访问为什么慢呢?

Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            //下标小于中间值,从头开始搜索
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            //下标大于中间值  从尾开始搜索
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

看到了吧,因为要遍历查找index的值,所以慢。

69 集合的equals只关心元素是否equals。

public boolean equals(Object o) {
        if (o == this)
            return true;
            //只要实现list接口即可
        if (!(o instanceof List))
            return false;

        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            //只要一个元素不equals就退出
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }

元素个数以及同索引上的元素equals才能说两个list equals。

public void suggest_69() {
		ArrayList<String> arrayList1 = new ArrayList();
		arrayList1.add("apple");
		arrayList1.add("banana");

		ArrayList<String> arrayList2 = new ArrayList();
		arrayList2.add("banana");
		arrayList2.add("apple");

		System.out.println(arrayList1.equals(arrayList2));

		ListIterator<String> e1 = arrayList1.listIterator();

		while(e1.hasNext()){
			String next = e1.next();
			System.out.println(next);
		}

		ListIterator<String> e2 = arrayList2.listIterator();

		while(e2.hasNext()){
			String next = e2.next();
			System.out.println(next);
		}
	}
false
apple
banana
banana
apple

Process finished with exit code 0

70 子列表只是原列表的一个视图。

public void suggest_70() {
		ArrayList<String> arrayList1 = new ArrayList();
		arrayList1.add("apple");
		arrayList1.add("banana");

		List<String> subList = arrayList1.subList(0, 1);
		subList.add("orange");

		System.out.println(subList.size());
		System.out.println(arrayList1.size());
	}
2
3

Process finished with exit code 0

看一下arrayList1.subList(0, 1)的源码

public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
    }

再看一下SubList

private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;

        SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }
			//操作子list
        public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }
		//操作list
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

        public int size() {
            checkForComodification();
            return this.size;
        }
		//操作的原数组
        public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }
		//操作的原数组
        public E remove(int index) {
            rangeCheck(index);
            checkForComodification();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }

arrayList1.subList的使用场景:

public void suggest_70() {
		ArrayList<String> arrayList1 = new ArrayList();
		arrayList1.add("apple");
		arrayList1.add("java");
		arrayList1.add("php");
		arrayList1.add("banana");

		List<String> subList = arrayList1.subList(1, 3);
		subList.clear();


		for (String item : arrayList1) {
			System.out.println(item);
		}
	}
apple
banana

Process finished with exit code 0

arrayList1.subList调用后要设置原列表只读,否则会出问题

public void suggest_71() {
		ArrayList<String> arrayList1 = new ArrayList();
		arrayList1.add("apple");
		arrayList1.add("banana");

		List<String> subList = arrayList1.subList(1,2);

		arrayList1.add("java");
		arrayList1.add("php");

		System.out.println(subList.size());
	}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
	at java.util.ArrayList$SubList.size(ArrayList.java:1040)
	at EffectiveJava.suggest_71(EffectiveJava.java:45)
	at EffectiveJava.main(EffectiveJava.java:31)

Process finished with exit code 1

subList.size()的第一件事实调用checkForComodification

private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
        }

modCount记录着原list的修改次数。这比较sublist创建时的modCount值和当前值是否相等,如果原list修改过,这里必然不等。
正确的姿势应该是

public void suggest_71() {
		ArrayList<String> arrayList1 = new ArrayList();
		arrayList1.add("apple");
		arrayList1.add("banana");

		List<String> subList = arrayList1.subList(1,2);
		//将原list置为只读
		List<String> unmodifiableList = Collections.unmodifiableList(arrayList1);
		arrayList1.add("java");
		arrayList1.add("php");

		System.out.println(subList.size());
	}

73.Comparable,Comparator和策略模式

实现Comparable 可以实现默认排序
实现Comparator可以实现实现多种排序

74.列表中检索的选择

public void suggest_73() {
		ArrayList<Student> students = new ArrayList();
		Student student1 = new Student(60,60);
		Student student2 = new Student(90,30);
		Student student3 = new Student(50,90);
		Student student4 = new Student(80,10);
		students.add(student1);
		students.add(student2);
		students.add(student3);
		students.add(student4);

		int i = students.indexOf(student3);//遍历查找。一般的场景,用这个足够。依据equals
		Collections.sort(students);//用默认排序
		int i1 = Collections.binarySearch(students, student3);//要先排序再查找。高性能要求的地方用这个。依据compareTo。实现compareTo就要复写equals

		System.out.println(i);
		System.out.println(i1);
	}

重写compareTo一定要重新equals,重写equals又要冲洗hashCode

77.集合乱序shuffle

Collections.shuffle(students);
实现随即打乱列表顺序。
用途:

  • 程序的伪装,比如游戏中群殴野怪的宝物分配策略
  • 用在抽奖程序
  • 用子安全传输

78.hashMap比ArrayList占内存

相同的元素个数hashMap比ArrayList占用内存更快,原因有两点:

  • hashMap把元素封装成entry
  • hashMap扩容是2倍扩容,List是1.5倍扩容

带有hash的集合查找速度更快。

第6章 枚举、注解

87.枚举用valueOf抛异常

传入的值不是枚举项的时候会抛出异常,解决办法有:

  • 在枚举中定义一个contains()方法
  • try-catch

87.枚举实现工厂模式

第7章 泛型、反射

99.泛型的多重界限

100.反射中选择getDeclaredxxx和getxxx

getDeclaredMethod:获得自身类的所有方法,不受访问权限限制
getMethod:获得public级别的方法,包括从父类继承来的

103.反射访问属性或者方法时将Accessible设置为true

在反射中,我们在invoke之前习惯先设置Accessible=true,这是为什么呢?

AccessibleObject是Field、Method、Constructor的父类,决定其是否可以快速访问而不进行访问权限检查。

public class AccessibleObject implements AnnotatedElement {
	//定义反射的默认操作权限
    static final private java.security.Permission ACCESS_PERMISSION =
        new ReflectPermission("suppressAccessChecks");
    //是否重置了安全检查  默认false
    boolean override;
        
public boolean isAccessible() {
        return override;
    }
}

在大量反射情况下,设置Accessibl=true可以提高性能20倍以上。

104.使用forName动态加载类文件

动态加载的意义:
加载一个类表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量工作,比如注册自己、初始化环境等。
经典的加载数据库驱动程序

看Driver类的源码

需要说明的是,forName只是把一个类加载到内存,之后初始化static代码是由类的加载机制决定的。

104.forName无法动态加载数组

在java中数组是一个非常特殊的类,虽然它是一个类,但是没有定义类的路径,所以forName无法加载。
那么数组的动态加载怎么搞?java提供了数组工具Array可以通过反射创建数组。

107.动态代理实现装饰模式

第8章 异常

107.关于try、catch、finally

看一个经典题目:

public static int demo5() {
        try {
            return printX();
        }
        finally {
            System.out.println("finally trumps return... sort of");
        }
    }
    public static int printX() {

        System.out.println("X");
        return 0;
    }
X
finally trumps return... sort of
0

参考:https://blog.csdn.net/u013309870/article/details/73498860

114.在构造函数中不要抛出异常

115.使用Throwable获取占信息

public void suggest_114() {
		Throwable throwable = new Throwable();
		StackTraceElement[] stackTrace = throwable.getStackTrace();
		for (StackTraceElement st:stackTrace){
			System.out.println(st.getClassName());
		}
	}

系统运行时异常或者new Throwable()时,jvm会生成Throwable对象,把当前的调用栈保存下来。因此我们可以new Throwable()来获得栈信息,比如调用者信息。
此方法常用作离线注册码校验,当破解者视图暴力破解时,由于主执行者不是期望的值,破解失败。

第9章 多线程和并发

118.不要复写start

看下start里边都干了啥

public synchronized void start() {
       //判断线程状态、必须是未启动状态
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
 //加入线程组
        group.add(this);

        boolean started = false;
        try {
            start0();//分配线程内存、启动线程、运行run
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    //本地方法,实现启动线程、申请栈内存、运行run
    private native void start0();

121.使用常量优先级

线程由10个优先级1-10。优先级越高获得执行的机会越大。

  • 优先级差别越大获得运行机会的差别越明显
  • Thread类中有三个优先级常量,够我们平时使用了。
    public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

122.用Thread.UncaughtExceptionHandler提高线程健壮性

如果我们希望线程出现异常后自动重启,怎么实现呢?

class MultiThreadExceptionHandler implements Thread.UncaughtExceptionHandler{

		@Override
		public void uncaughtException(Thread t, Throwable e) {
			e.printStackTrace();
			new MultiThread().start();
		}
	}

但是也要注意线程是因为什么原因挂掉的,有些原因,暴力重启只会徒增系统负担。

  • 死锁
  • 脏数据
  • oom

123.volatile只能保证更新的原子性

volatile不能保证数据同步
两个线程都更新volatile count
可能两个线程同时读取到count=1,然后都执行count++
执行了两次count++我们期望count=3,单count=2

124.推荐使用callable

实现callable表示可调度。
用callable实现异步调用比runnable有点在于

  • 有返回值

127.lock和Synchronized

第10章 性能、效率

132.提升java性能的基本方法

  • 不要在循环中计算
//每次循环都要计算count*2
while(i < count*2){
   //do somthing
}

应该替换为

int total = count*2;
while(i < total){
   //do somthing
}
  • 尽可能把变量,方法声明为final static
 public String toChineseNum(int num){
	//中文
	String[] cns = {"壹""贰","叁","肆","伍"};
	return cns[num+1];
}

每次调用该方法都要生成cns数组。其实这里将cns声明为final static更合适。

  • 缩小变量的作用范围
    目的是help GC。考虑变量是否可以放在以下范围:
    方法内 - 循环体内 - try catch内

  • 使用非线性检索
    比如,有序的ArrayList检索,使用binarySearch效率好于indexof

  • 不建立冗余对象

public void doSomething(){
   //异常信息
   String msg = "help me";
   try{
   	Thread.sleep(10);
   }catch (Exception e){
   	throw new MyException(e,msg);
   }
}

这段代码汇中,msg在catch中创建才合适。

133.clone对象并不比new高效

clone也可以生成对象,但是性能并不比new好。因为80%的对象都是new出来的,所以java的缔造者对new做了大量优化工作,那么clone的使用场景是什么呢?
用于构造函数比较复杂,对象属性比较多,通过new创建对象比较耗时间的场景。

137.JVM调优

  • 调整堆内存大小
    栈空间:线程开辟、线程结束、JVM回收,一般不会对性能有大影响,不足时会报StackOverFlowError,可以通过java -Xss 设置大熊啊
    堆空间:太小会频繁full GC甚至系统无法使用,太大会产生系统不稳定状况。
    可通过java -Xmx1024m -Xms512m设置初始堆内存512m,最大堆内存1G

  • 调整堆内存中个分区的比例
    堆内存包括三部分:新生区、老年区、永久区。
    新生区又包含三部分:伊甸区、幸存区1、幸存区2
    一般新生区:老年区比例1:3左右
    用以下命令设置
    java -XX:NewSize=32m -XX:MaxNewSize=640m -XX:MaxPermSize=1280m -XX:NewRatio=5
    该配置指定新生区最小内存32m,最大不超过640m,养老区最大不超过1280m,新生区和养老区比例1:5.

  • 变更垃圾的回收策略

  • 更换JVM
    目前市面上比较流行的JVM有三个产品
    java HotSpot VM
    Oracle JRocle JVM
    IBM JVM

第11章 开源世界

141.Apache扩展包

  • Bag
    是Collections的中一种,可以容纳重复元素,可统计相同元素的数量
   	Bag bag = new HashBag(Lists.newArrayList("red","red","red","blue"));
   	bag.getCount("red");
  • 双向Map
    JDK中的Map要求键唯一,双向Map要求键、值都唯一。
    org.apache.commons.collections.BidiMap

  • LazyMap:

LazyMap 意思就是这个Map中的键/值对一开始并不存在,当被调用到时才创建。

我们这样来理解,我们需要一个Map,但是由于创建成员的方法很“重”(比如数据库访问)

我们只有在调用get()时才知道如何创建,或者Map中出现的可能性很多。

我们无法在get()之前添加所有可能出现的键/值对。

我们觉得没有必要去初始化一个Map而又希望它在必要时自动处理数据。

关于LazyMap的使用再说两点,一般需要使用LazyMap.decorate(Map,Factory)的方式来创建,实现Factory中的create方法。2章 思想为源

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

改善java程序的151个建议 - 读书笔记 的相关文章

随机推荐