读书笔记----《编写高质量代码:改善Java程序的151个建议》第四/五章

最近变得好懒,一定要坚持写下去。

字符串

52:推荐使用String直接量赋值
public class Client{
public static void main(String[]args){
String str1="中国";
String str2="中国";
String str3=new String("中国");
String str4=str3.intern();
//两个直接量是否相等
boolean b1=(str1==str2);
//直接量和对象是否相等
boolean b2=(str1==str3);
//经过intern处理后的对象与直接量是否相等
boolean b3=(str1==str4);
}
}

intern会检查当前的对象在对象池中是否有字面值相同的引用对象,如果有则返回池中对象,如果没有则放置到对象池中,并返回当前对象。

53:注意方法中传递的参数要求

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

54:正确使用String、StringBuffer、StringBuilder

String是不可变字符
StringBuffer是线程安全的,StringBuilder是线程不安全的
(1)使用String类的场景
在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算等。
(2)使用StringBuffer类的场景
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程的环境中,例如XML解析、HTTP参数解析和封装等。
(3)使用StringBuilder类的场景
在频繁进行字符串的运算,并且运行在单线程的环境中,如SQL语句的拼装、JSON封装等。

55:注意字符串的位置
public static void main(String[]args){
String str1=1+2+"apples";
String str2="apples:"+1+2;
}

在“+”表达式中,String字符串具有最高优先级。

56:自由选择字符串拼接方法

对一个字符串进行拼接有三种方法:加号、concat方法及StringBuilder(或StringBuffer)
字符串拼接方式中,append方法最快,concat方法次之,加号最慢

public String concat(String str){
int otherLen=str.length();
//如果追加的字符串长度为0,则返回字符串本身
if(otherLen==0){
return this;
}
//字符数组,容纳的是新字符串的字符
char buf[]=new char[count+otherLen];
//取出原始字符串放到buf数组中
getChars(0count, buf,0);
//追加的字符串转化成字符数组,添加到buf中
str.getChars(0,otherLen, buf, count);
//复制字符数组,产生一个新的字符串
return new String(0count+otherLen, buf);
}

每次的concat操作都会新创建一个String对象,这就是concat速度慢下来的真正原因。
用+号事实上相当于创建了一个StringBuilder对象,但如果在循环中使用就会频繁创建对象,这也是它最慢的原因。

57:推荐在复杂字符串操作中使用正则表达式
58:强烈建议使用UTF编码

(1)Java文件编码
文件的编码格式就是操作系统默认的格式,如果是使用IDE工具创建的,则依赖于IDE的设置。
(2)Class文件编码
.class的文件是UTF-8编码的UNICODE文件,在任何操作系统上都是一样的。
注意: javac-encoding GBK Client.java 这里显式声明的是Java文件的编码格式,而不是class文件。

59:对字符串排序持一种宽容的心态

对中文的排序并不是一个简单的事情

public static void main(String[]args){
String[]strs={"张三(Z)""李四(L)""王五(W)"};
//排序,默认是升序
Arrays.sort(strs);
int i=0;
for(String str:strs){
System.out.println((++i)+"、"+str);
}
}
//输出结果:
1、张三(Z)
2、李四(L)
3、王五(W)

Arrays工具类的默认排序是通过数组元素的compareTo方法来进行比较的,那我们来看String类的compareTo的主要实现:

while(k<lim){
//原字符串的字符数组
char c1=v1[k];
//比较字符串的字符数组
char c2=v2[k];
if(c1!=c2){
//比较两者的char值大小
return c1-c2;
}
k++;
}
//注意这里是字符比较(减号操作符),也就是UNICODE码值的比较,
//查一下UNICODE代码表,“张”的码值是5F20,
//而“李”是674E,这样一看,“张”排在“李”的前面也就很正确了

Java推荐使用Collator类进行排序:

public static void main(String[]args)throws Exception{
String[]strs={"张三(Z)""李四(L)""王五(W)"};
//定义一个中文排序器
Comparator c=Collator.getInstance(Locale.CHINA);
//升序排列
Arrays.sort(strs, c);
int i=0;
for(String str:strs){
System.out.println((++i)+"、"+str);
}
}
//输出结果:
1、李四(L)
2、王五(W)
3、张三(Z)
public static void main(String[]args)throws Exception{
String[]strs={"犇(B)""鑫(X)"};
Arrays.sort(strs, Collator.getInstance(Locale.CHINA));
int i=0;
for(String str:strs){
System.out.println((++i)+"、"+str);
}
}
//输出结果:
1、鑫(X)
2、犇(B)

Java使用的是UNICODE编码,而中文UNICODE字符集是来源于GB18030的,GB18030又是从GB2312发展起来,GB2312是一个包含了7000多个字符的字符集,它是按照拼音排序,并且是连续的,之后的GBK、GB18030都是在其基础上扩充出来的,所以要让它们完整排序也就难上加难了。
如果是排序对象是经常使用的汉字,使用Collator类排序完全可以满足我们的要求,如果需要严格排序,则要使用一些开源项目来自己实现了,比如pinyin4j可以把汉字转换为拼音,然后我们自己来实现排序算法。

//pinyin4j基本用法:
String[] pinyinArray =PinyinHelper.toHanyuPinyinStringArray('单');
for(int i = 0; i < pinyinArray.length; ++i)
{
System.out.println(pinyinArray[i]);
}
//输出结果:
dan1
chan2
shan4

数组和集合

60:性能考虑,数组是首选

在实际测试中发现:对基本类型进行求和计算时,数组的效率是集合的10倍。

61:若有必要,使用变长数组

在实际开发中,如果确实需要变长的数据集,数组也是在考虑范围之内的,不能因固定长度而将其否定之。

public static<T>T[]expandCapacity(T[]datas, int newLen){
//不能是负值
newLen=newLen<0?0:newLen;
//生成一个新数组,并拷贝原值
return Arrays.copyOf(datas, newLen);
}
62:警惕数组的浅拷贝

通过Arrays.copyOf() 方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。需要说明的是,数组的clone方法同样是浅拷贝,而且集合的clone方法也都是浅拷贝

63:在明确的场景下,为集合指定初始容量

ArrayList是一个大小可变的数组,但它在底层使用的是数组存储(也就是elementData变量),而且数组是定长的,要实现动态长度必然要进行长度的扩展,ensureCapacity方法提供了此功能,代码如下:

public void ensureCapacity(int minCapacity){
//修改计数器
modCount++;
//上次(原始)定义的数组长度
int oldCapacity=elementData.length;
//当前需要的长度超过了数组长度
if(minCapacity>oldCapacity){
Object oldData[]=elementData;
//计算新数组长度
int newCapacity=(oldCapacity*3)/2+1;
if(newCapacity<minCapacity)
newCapacity=minCapacity;
//数组拷贝,生成新数组
elementData=Arrays.copyOf(elementData, newCapacity);
}
}

注意: newCapacity=(oldCapacity*3)/2+1;
使用new ArrayList(),则elementData的初始长度就是10

64:多种最值算法,适时选择
65:避开基本类型数组转换列表陷阱
public static void main(String[]args){
int[]data={12345};
List list=Arrays.asList(data);
System.out.println("列表中的元素数量是:"+list.size());
//输出长度为1
}

注意 原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱。
修改:

public static void main(String[]args){
Integer[]data={12345};
List list=Arrays.asList(data);
System.out.println("列表中的元素数量是:"+list.size());
}

在把基本类型数组转换成列表时,要特别小心asList方法的陷阱,避免出现程序逻辑混乱的情况。

66:asList方法产生的List对象不可更改
List<String>names=Arrays.asList("张三""李四""王五");
//使用错误,改成:
List<String>names=Lists.newArrayList(Arrays.asList("张三""李四""王五"));
67:不同的列表选择不同的遍历方法

遍历ArrayList使用下标要比foreach或者Iterator性能提升65%左右。这是因为ArrayList数组实现了RandomAccess接口,RandomAccess和Cloneable、Serializable一样,都是标志性接口,不需要任何实现,实现了RandomAccess则表明这个类可以随机存取,对我们的ArrayList来说也就标志着其数据元素之间没有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储。
迭代器是23个设计模式中的一种,“提供一种方法访问一个容器对象中的各个元素,同时又无须暴露该对象的内部细节”,也就是说对于ArrayList,需要先创建一个迭代器容器,然后屏蔽内部遍历细节,对外提供hasNext、next等方法。问题是ArrayList实现了RandomAccess接口,已表明元素之间本来没有关系,可是,为了使用迭代器就需要强制建立一种互相“知晓”的关系,比如上一个元素可以判断是否有下一个元素,以及下一个元素是什么等关系,这也就是通过foreach遍历耗时的原因。
ava为ArrayList类加上了RandomAccess接口,就是在告诉我们,“嘿,ArrayList是随机存取的,采用下标方式遍历列表速度会更快”。
另外:如果是LinkedList采用下标方式获取每次get都会引起一次遍历,效率则无从谈起。

68:频繁插入和删除时使用LinkedList
69:列表相等只需关心元素数据
public static void main(String[]args){
ArrayList<String>strs=new ArrayList<String>();
strs.add("A");
Vector<String>strs2=new Vector<String>();
strs2.add("A");
System.out.println(strs.equals(strs2));
}

实现了List接口的只判断元素。

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

List接口提供了subList方法,其作用是返回一个列表的子列表,这与String类的subString有点类似。

public static void main(String[]args){
//定义一个包含两个字符串的列表
List<String>c=new ArrayList<String>();
c.add("A");
c.add("B");
//构造一个包含c列表的字符串列表
List<String>c1=new ArrayList<String>(c);
//subList生成与c相同的列表
List<String>c2=c.subList(0,c.size());
//c2增加一个元素
c2.add("C");
System.out.println("c==c1?"+c.equals(c1));
System.out.println("c==c2?"+c.equals(c2));
}
//输出结果:
c==c1?false
c==c2?true

来看subList源码:

public List<E>subList(int fromIndex, int toIndex){
returnthis instanceof RandomAccess?
new RandomAccessSubList<E>(this, fromIndex, toIndex):
new SubList<E>(this, fromIndex, toIndex));
}

subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,(随机存储的使用频率比较高),而且RandomAccessSubList也是SubList子类,所以所有的操作都是由SubList类实现的(除了自身的SubList方法外),直接来看SubList类的代码:

class SubListEextends AbstractListE>{
//原始列表
private AbstractList<E>l;
//偏移量
private int offset;
//构造函数,注意list参数就是我们的原始列表
SubList(AbstractList<E>list, int fromIndex, int toIndex){
/*下标校验,省略*/
//传递原始列表
l=list;
offset=fromIndex;
//子列表的长度
size=toIndex-fromIndex;
}
//获得指定位置的元素
public E get(int index){
/*校验部分,省略*/
//从原始字符串中获得指定位置的元素
return l.get(index+offset);
}
//增加或插入
public void add(int index, E element){
/*校验部分,省略*/
//直接增加到原始字符串上
l.add(index+offset, element);
/*处理长度和修改计数器*/
}
/*其他方法省略*/
}

参考代码,subList方法的实现原理:它返回的SubList类也是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,子列表只是原列表的一个视图(View),所有的修改动作都反映在了原列表上。

c与c1不相等是因为通过ArrayList构造函数创建的List对象c1实际上是新列表,它是通过数组的copyOf动作生成的,所生成的列表c1与原列表c之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就是说元素是深拷贝的)。
注意 subList产生的列表只是一个视图,所有的修改动作直接作用于原列表。

71:推荐使用subList处理局部列表

需求:一个列表有100个元素,现在要删除索引位置为20~30的元素。

public static void main(String[]args){
//初始化一个固定长度,不可变列表
List<Integer>initData=Collections.nCopies(1000);
//转换为可变列表
ArrayList<Integer>list=new ArrayList<Integer>(initData);
//删除指定范围的元素
list.subList(2030).clear();
}

上一个建议讲解了subList方法的具体实现方式,所有的操作都是在原始列表上进行的,那我们就用subList先取出一个子列表,然后清空。因为subList返回的List是原始列表的一个视图,删除这个视图中的所有元素,最终就会反映到原始字符串上。

72:生成子列表后不要再操作原列表
public static void main(String[]args){
List<String>list=new ArrayList<String>();
list.add"A");
list.add"B");
list.add"C");
List<String>subList=list.subList02);
//原字符串增加一个元素
list.add"D");
System.out.println"原列表长度:"+list.size());
System.out.println"子列表长度:"+subList.size());
}
//subList的size方法出现了异常
原列表长度:4
Exception in thread"main"java.util.ConcurrentModifcationException
at java.util.SubList.checkForComodification(AbstractList.java752)
at java.util.SubList.size(AbstractList.java625

size的源代码:

public int size(){
checkForComodifcation();
return size;
}
//checkForComodification方法就是用于检测是否并发修改的
private void checkForComodification(){
//判断当前修改计数器是否与子列表生成时一致
if(l.modCount!=expectedModCount)
throw new ConcurrentModificationException();
}

在生成子列表后再修改原始列表,l.modCount的值就必然比expectedModCount大1.
subList的其他方法也会检测修改计数器,例如set、get、add等方法,若生成子列表后,再修改原列表,这些方法也会抛出ConcurrentModificationException异常。
最有效的办法就是通过Collections.unmodifiableList方法设置列表为只读状态,代码如下:

public static void main(String[]args){
List<String>list=new ArrayList<String>();
List<String>subList=list.subList(02);
//设置列表为只读状态
list=Collections.unmodifableList(list);
//对list进行只读操作
doReadSomething(list//对subList进行读写操作
doReadAndWriteSomething(subList)
}

List也可以有多个视图(子列表),但问题是只要生成的子列表多于一个,则任何一个子列表就都不能修改了,否则就会抛出ConcurrentModificationException异常。

73:使用Comparator进行排序

按职位临时倒序排列呢?注意只是临时的,是否要重写一个排序器?完全不用,有两个解决办法:
直接使用Collections.reverse(List<?>list)方法实现倒序排列。
通过Collections.sort(list, Collections.reverseOrder(new PositionComparator()))也可以实现倒序排列。
学会使用apache的工具类来处理排序:

public int compareTo(Employee o){
return new CompareToBuilder()
.append(position, o.position)//职位排序
.append(id, o.id).toComparison();//工号排序
}

关于CompareToBuilder:
int comparison;存储比较的结果。比较的对象分左值和右值,当左值小于右值时,该值为-1,否则该值为0;相等则为0。

74:不推荐使用binarySearch对列表进行检索

对一个列表进行检索时,我们使用得最多的是indexOf方法。Collections工具类也提供一个检索方法:binarySearch,该方法也是对一个列表进行检索的,可查找出指定值的索引值,但是在使用这个方法时就有一些注意事项了:
二分法查询的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。不排序怎么确定是在小区(比中间值小的区域)中查找还是在大区(比中间值大的区域)中查找呢?二分法查找必须要先排序,这是二分法查找的首要条件。
在大数据集而且目标值又接近尾部时,从性能方面考虑,binarySearch是最好的选择。

75:集合中的元素必须做到compareTo和equals同步

(1)indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找。
(2)equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同。
既然一个是决定排序位置,一个是决定相等,那我们就应该保证当排序位置相同时,其equals也相同,否则就会产生逻辑混乱。
注意 实现了compareTo方法,就应该覆写equals方法,确保两者同步。

76:集合运算时使用更优雅的方式
public static void main(String[]args){
List<String>list1=new ArrayList<String>();
list1.add("A");
list1.add("B");
List<String>list2=new ArrayList<String>();
list2.add("C");
list2.add("B");
//并集
list1.addAll(list2);
//交集,list1中包含list1、list2中共有的元素
list1.retainAll(list2);
//差集,从list1中删除出现在lis2的元素
list1.removeAll(list2);
//无重复的并集
//删除在list1中出现的元素,把剩余的list2元素加到list1中
list2.removeAll(list1);
list1.addAll(list2);
}
77:使用shuffle打乱列表
public static void main(String[]args){
int tagCloudNum=10;
List<String>tagClouds=new ArrayList<String>(tagCloudNum);
//打乱顺序
Collections.shuffe(tagClouds);
}

使用场景
(1)可以用在程序的“伪装”上。
比如标签云,或者是游戏中的打怪、修行、群殴时宝物的分配策略。
(2)可以用在抽奖程序中。
先使用shuffle把员工排序打乱,每人中奖几率就是相等的了。
(3)可以用在安全传输方面。
比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后自行排序,即可实现即使是相同的数据源,也会产生不同密文的效果,加强了数据的安全性。

78:减少HashMap中元素的数量
public static void main(String[]args){
Map<String, String>map=new HashMap<String, String>();
final Runtime rt=Runtime.getRuntime();
//JVM终止前记录内存信息
rt.addShutdownHook(new Thread(){
@Override
public void run(){
StringBuffer sb=new StringBuffer();
long heapMaxSize=rt.maxMemory()>>20;
sb.append"最大可用内存:"+heapMaxSize+"M\n");
long total=rt.totalMemory()>>20;
sb.append"堆内存大小:"+total+"M\n");
long free=rt.freeMemory()>>20;
sb.append"空闲内存:"+free+"M");
System.out.println(sb);
}
});
//放入近40万键值对
for(int i=0;i<393217;i++){
map.put"key"+i,"vlaue"+i);
}
}
// 报错
Exception in thread"main"最大可用内存:63M
java.lang.OutOfMemoryError:Java heap space
at java.util.HashMap.resize(HashMap.java462)
at java.util.HashMap.addEntry(HashMap.java755)
at java.util.HashMap.put(HashMap.java385)
at Client.main(Client.java24)
堆内存大小:63M
空闲内存:7M

同样的程序,只是把HashMap修改成了List,增加的字符串元素也相同,却不会报错。
原因:HashMap比ArrayList多了一个层Entry的底层对象封装,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会依据阀值判断规则进行判断,因此相对于ArrayList来说,它就会先出现内存溢出。
注意 尽量让HashMap中的元素少量并简单。

79:集合中的哈希码不要重复

两个不同的集合容器,一个是ArrayList,一个是HashMap,都是插入10000个元素,然后判断是否包含最后一个加入的元素,HashMap比ArryList快了40多倍
首先介绍HashMap的table数组是如何存储元素的:
1)table数组的长度永远是2的N次幂。
2)table数组中的元素是Entry类型。
3)table数组中的元素位置是不连续的。
hashMap的存储本质上是使用了数据结构中类似“数组列表”的结构这里写图片描述
并且,hash码重复后不止插入效率会变慢,查找效率也会变得和ArrayList相当。所以要尽量避免hash码重复的情况。

80:多线程使用Vector或HashTable
81:非稳定排序推荐使用List

使用SortedSet的时候一定要特别熟注意,不要使用它给可以更新的元素排序
SortedSet接口(TreeSet实现了该接口)只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String、Integer等类型,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。
我们之所以使用TreeSet是希望实现自动排序,即使修改也能自动排序,既然它无法实现,那就用List来代替,然后再使用Collections.sort()方法对List排序。

82:由点及面,一叶知秋—集合大家族

1)List
实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则。
(2)Set
Set是不包含重复元素的集合,其主要的实现类有:EnumSet、HashSet、TreeSet,其中EnumSet是枚举类型的专用Set,所有元素都是枚举类型;HashSet是以哈希码决定其元素位置的Set,其原理与HashMap相似,它提供快速的插入和查找方法;TreeSet是一个自动排序的Set,它实现了SortedSet接口。
(3)Map
Map是一个大家族,它可以分为排序Map和非排序Map,排序Map主要是TreeMap类,它根据Key值进行自动排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子类,它的主要用途是从Property文件中加载数据,并提供方便的读写操作;EnumMap则是要求其Key必须是某一个枚举类型。
Map中还有一个WeakHashMap类需要说明,它是一个采用弱键方式实现的Map类,它的特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,也就是说使用WeakHashMap装载数据不用担心内存溢出的问题,GC会自动删除不用的键值对,这是好事。但也存在一个严重问题:GC是静悄悄回收的(何时回收?God knows!),我们的程序无法知晓该动作,存在着重大的隐患。
(4)Queue
队列,它分为两类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组建的队列,LinkedBlockingQueue是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,我们最经常使用的是PriorityQueue类。
还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDeque、LinkedBlockingDeque、LinkedList。
(5)数组
数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行,更重要的一点就是所有的集合底层存储的都是数组。
(6)工具类
数组的工具类是java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections,有了这两个工具类,操作数组和集合会易如反掌,得心应手。
(7)扩展类
使用Apache的commons-collections扩展包,和Google的google-collections扩展包

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值