您的位置:首页 > 编程语言 > Java开发

[笔记]改善Java程序的151个建议---第五章 数组和集合

2016-01-29 00:00 591 查看
建议60:性能考虑,数组是首选
集合类的底层通过数组实现。
集合在运算过程中要做拆箱动作,Integer自动通过intValue转换成int,性能消耗于此。
基本类型求和操作,数组的效率是集合的10倍。
public static int sum(int[] datas){
int sum = 0;
for(int i = 0; i < datas.length ; i++)
sum += datas[i];
return sum;
}

建议61:若有必要,使用变长数组
Java的数组是定长的,一旦经过初始化就不可能改变长度。实际应用中非常不方便。可以通过对数组的扩容解决这个问题。
//变长数组,解决数组固定长度问题
public static <T> T[] expandCapacityArray(T[] datas, int newLen){
//不能是负值
int newLens = newLen<0? 0 : newLen;
//生成一个新数组,并拷贝原值
return Arrays.copyOf(datas, newLens);
}

//使用方法
public static void main(String[] args){
//一个班级最多60个学生
Stu[] classes = new Stu[60];
//classes初始化
...

//容量扩展,一个班级可以容纳80人,数组加长
classes = expandCapacity(classes, 80);
//重新初始化后超过限额20人
}

建议62:警惕数组的浅拷贝
第一个箱子里面有7个颜色的气球,现在希望在第二个箱子里面也放入7个颜色气球,把最后一个气球的颜色修改为蓝色。有浅拷贝问题。

public enum Color {
Red, Oranger, Yellow, Green, Indigo, Bule, Voioet;
}

public class Ballon {
private int id;
private Color color;

Ballon(int _id , Color _color){
this.id = _id;
this.color = _color;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public Color getColor() {
return color;
}

public void setColor(Color color) {
this.color = color;
}

public String toString() {
return "编号" + " " + this.getId() + "\t" + "颜色" + " " + this.getColor();
}
}

import java.util.Arrays;

public class Client {

public static void main(String[] args) {
//气球数量
int ballonNum = 7;
//第一个箱子
Ballon[] box1 = new Ballon[ballonNum];
&
3ff0
nbsp; //初始化第一个箱子里的气球
for(int i = 0 ; i < ballonNum ; i++){
box1[i] = new Ballon(i , Color.values()[i]);
}
//深拷贝。重新生成一个气球对象。作为第二个箱子。
Ballon[] box2 = new Ballon[ballonNum];
//深拷贝。拷贝第一个箱子的气球颜色到第二个箱子对象。
for(int i = 0 ; i < box1.length ; i++)
box2[i] = new Ballon(box1[i].getId(), box1[i].getColor());

//第二个箱子的气球是拷贝第一箱子里的。浅拷贝问题,会造成修改box2 color的时候,box1相应位置的color也被改变
//box2 = Arrays.copyOf(box1, box1.length);
//修改最后一个气球颜色
box2[6].setColor(Color.Bule);
//打印第一个箱子的气球颜色
for(Ballon b : box1)
System.out.println(b);

System.out.println("---------------------");
//打印第二个箱子的气球颜色
for(Ballon b : box2)
System.out.println(b);
}
}

建议63;在明确的场景下,为集合初始化时声明容量
ArrayList,Vector,HashMap,一般直接用new生成集合,并且长度自动管理。

ArrayList底层使用数组存储。要实现动态长度,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);
}
}

数组长度扩展1.5倍。
elementData默认长度是10.new ArrayList()后,elementData初始长度是10.如果,不设置初始容量,系统就按照1.5倍的规则扩容,每次扩容都是一次数组拷贝,如果数据量大,这样的拷贝非常耗费资源。尽可能设置一个ArrayList可能的长度。

Vector,使用ensureCapacityHelper计算数组长度
private void ensureCapacityHelper(int minCapacity){
int oldCapacity = elementData.length;
if(minCapacity > oldCapacity){
Object[] oldData = elementData;
//如果有递增步长,按照步长增长,否则扩容2倍
int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) : (oldCapacity * 2);
//越界检查,否则超过int最大值
if(newCapacity < minCapacity){
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
Vector月ArrayList不同的地方是提供了步长增长,其意思是每次数组拓长时要增加的长度。不设置这个值的话,则容量翻倍。

HashMap按照倍数扩展
Stack继承自Vector

建议64:多种最值算法,适时选择
对一批数据排序,找出其中最大值,最小值。
以求最大值为例,解释多种算法。
(1)自行实现,快速查找最大值
public static int max(int[] data){
int max = data[0];
for(int i : data)
max = max > i ? max : i;
return max;
}

(2)先排序,后取值
public static int max(int[] data){
//先排序
Arrays.sort(data.clone());//为什么先要clone()一下,因为数组也是一个对象,不拷贝而排序的话,就改变了原有数组的顺序。
//后取值
return data[data.length - 1];
}

//最值计算,集合类最简单,数组性能最好
//剔除重复元素,获得第二大的值
public static Integer getSecond(Integer[] datas){
//转换为列表
List<Integer> list = Arrays.asList(datas);
//转换为TreeSet,删除重复元素并升序排序
TreeSet<Integer> set = new TreeSet<>(list);
//取得比最大值小的值
return set.lower(set.last());
}

建议65:避开基本类型数据转换列表陷阱
经常使用Arrays和Collections两个工具类在数组和列表之间转换
public static<T> List<T> asList(T... a){
return new ArrayList<T>(a);
}
asList输入的是一个泛型变长参数,但是基本类型是不能泛化的。所以,8个基本类型不能作为泛型参数。想要作为泛型参数,必须使用其所对应的包装类型。

public static void main(String[] args){
//返回的数量是5
Integer[] data = {1,2,3,4,5};
//这个不可以,返回的数量是1
//int[] data = {1,2,3,4,5};

List list = Arrays.asList(data);
System.out.println("the number of Array is : " + list.size());
}

建议66:asList方法产生的List对象不可更改
asList方法返回的列表有特殊的地方
enum Week{Sun,Mon,Tue,Wed,Thu,Fri,Sat}
public static void main(String[] args){
//5天工作
Week[] workDays = {Week.Mon, Week.Tue, Week.Wed, Week.Thu, Week.Fri};
//转换列表
List<Week> list = Arrays.asList(workDays);
//增加周六
list.add(Week.Sat); //程序报错
}

public static<T> List<T> asList(T... a){
return new ArrayList<T>(a);
}
这里面的ArrayList不是java.util.ArrayList,而是Arrays工具类的一个内置类。除了Arrays能访问,其他类都不能访问。其没有add方法,其父类AbstractList提供了add的抽象。其列表的长度无法更改。

ArrayList静态内部类,只能如下5种方法,size,toArray,get,set,contains。
Integer[] data = {1,2,3,4,5,6,7,8};
List<Integer> list = Arrays.asList(datas);
list.add(9) 报UnsupportedOperationException()

//建议67:不同列表选择不同遍历方法
ArrayList实现了RandomAccess接口,是一个可以随机存取的List。两个相邻的元素没有关联,没有相互依赖和索引关系,可以随机访问和存取。采用下标遍历方式,速度最快。
LinkedList是顺序存取的,元素之间有关联关系,foreach,迭代器方式速度最快。
public static int average(List<Interger> list){
int sum = 0;
if(list instanceof RandomAccess) {
for(int i = 0, size = list.size(); i < size ; i++)
sum += list.get(i);
else
for(int i : list)
sum += i;
}

return sum/list.size();
}

//建议68:频繁插入删除(写操作)时候用LinkedList
LinkedList双向链表.
LinkedList是顺序存取的,定位元素必定是一个遍历的过程。效率低。做修改操作不适合,ArrayList随机存取,适合修改操作。

// 建议69:列表相等,只需关系元素数据,不用考虑集合类型
接口必须用instanceof
ArrayList<String> list = new ArrayList<>();
list.add("a");
Vector<String> vector = new Vector<>();
vector.add("a");
list.equals(vector);
是相等的

//建议70:子列表是原列表的一个视图
List<String> c = new ArrayList<>();

c.add("a");
c.add("b");

List<String> c1 = new ArrayList<>(c);
List<String> c2 = c.subList(0, c.size());

c2.add("c");
System.out.println(c.equals(c1) + " " + c.equals(c2));

//建议71:用subList处理局部列表
一个列表有100个元素,删除20-30之间的元素
List<Integer> initData = Collections.nCopies(100, 0);//初始化一个固定长度的列表
List<Integer> list = new ArrayList<>(initData);//转化为一个可变长度的列表
list.subList(20, 30).clear();

//建议72:生成子列表后,不要操作原列表
会报错
对原列表进行只读操作,对子列表进行读写操作。
List<String> list = new ArrayList<>();

list.add("a");
list.add("b");
List<String> subList = list.subList(0,2);
//list readonly
list = Collections.unmodifiableList(list);

//sublist read & write
subList.add("c");
System.out.println(list.size() + " " + subList.size());

//建议73:用Comparator进行排序
升序降序
2种方法,Comparable接口作为实现类自身内部的排序,Comparator接口一个类的扩展排序工具,比较器,跟原有类的逻辑没有关系,实现2个类之间的比较逻辑。

Collections.sort()有一个重载方法Collections.sort(List<T> list, Comparator<? super T> c)可以接受比较类

临时倒排序
Collections.reverse(List<?> list)
Collections.sort(list, Collections.reverseOrder(new PositionComparator()))

先按照职位排序,再按照工号排序
在compareTo或compare方法中先判断职位是否相等,再判断工号

Collections.sort排序依赖compareTo的返回值,返回负数,当前值比对比值小;零表示相等;正数表示当前值比对比值大。

public enum Position {
Boss, Manager, Staff;
}

import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

public class Employee implements Comparable<Employee> {
private int id;
private String name;
private Position position;

public Employee(int _id, String _name, Position _position){
this.id = _id;
this.name = _name;
this.position = _position;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Position getPosition() {
return position;
}

public void setPosition(Position position) {
this.position =
3ff0
position;
}

@Override
public int compareTo(Employee e) {
//return new CompareToBuilder().append(position, e.position).append(id, e.id).toComparison();//自然升序
return new CompareToBuilder().append(e.position, position).append(e.id, id).toComparison();//自然降序
}

public String toString(){
return ToStringBuilder.reflectionToString(this);

}

}

import java.util.Comparator;

public class PositionComparator implements Comparator<Employee>{

@Override
public int compare(Employee arg0, Employee arg1) {
return arg0.getPosition().compareTo(arg1.getPosition());
}

}

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;
import java.util.RandomAccess;

public class Client {
public static void main(String[] args) {
List<Employee> list = new ArrayList<>(5);
list.add(new Employee(1004, "yao4", Position.Staff));
list.add(new Employee(1000,"yao",Position.Boss));
list.add(new Employee(1003, "yao3", Position.Staff));
list.add(new Employee(1001, "yao1", Position.Manager));

list.add(new Employee(1002, "yao2", Position.Manager));

//Collections.sort(list);//Employee的id排序
Collections.sort(list, new PositionComparator());//Employee的position排序

for(Employee e : list)
System.out.println(e);

}
}

//建议74:不推荐使用binarySearch对列表进行检索
对列表检索用的最多的是indexOf方法
binarySearch使用的先觉条件是list必须要先升序排列
不能直接对数据库中的业务数据进行排序,因为有业务规则,会改变原来的业务规则。
可以拷贝到一个数组,然后在排序,再使用binarySearch.
binarySearch性能比indexOf高很多。

建议75: 集合中的元素必须做到compareTo和equals同步
实现Comparable接口的元素可以排序,CompareTo方法是必要的实现,与equals方法有关系,当CompareTo返回0的时候,两个元素是相等的。
indexOf是通过equals方法判断的。equals判断元素是否相等。
binarySearch是通过compareTo方法的返回值判断的。compareTo判断元素在排序中的位置是否相同。
当覆盖CompareTo和equals方法的时候,判断的元素必须一致。

import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;

public class City implements Comparable<City>{
String code;
String name;

public City(String _code, String _name){
this.code = _code;
this.name = _name;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public int compareTo(City c){
// TODO Auto-generated method stub
return new CompareToBuilder().append(this.getName(), c.getName()).toComparison();
}

public boolean equals(Object obj){
if(obj == null)
return false;
if(obj == this)
return true;
if(obj.getClass() != getClass())
return false;

City city = (City) obj;

return new EqualsBuilder().append(this.getName(), city.getName()).isEquals();

}

public String toString(){
return ToStringBuilder.reflectionToString(this);
}
}

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;
import java.util.RandomAccess;

public class Client {
public static void main(String[] args) {

List<City> cities = new ArrayList<>();

cities.add(new City("200", "Shanghai"));
cities.add(new City("100", "Beijing"));
cities.add(new City("100", "Guangzhou"));

Collections.sort(cities);
for(City list : cities)
System.out.println(list);

City city = new City("100", "Beijing");

int index1 = cities.indexOf(city);
int index2 = Collections.binarySearch(cities, city);
System.out.println("indexOf " + index1);
System.out.println("binarySearch " + index2);

}
}

//建议76:集合运算时候使用更优雅的方式
并集,交集,差集
//并集,A+B
List<String> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
List<String> list2 = new ArrayList<>();
list2.add("c");
list2.add("d");
list1.allAll(list2); //并集

//交集,A,B共有的元素
list1.retainAll(list2);

//差集,属于A但不属于B的元素
list1.removeAll(list2);

//无重复的并集,A+B,如过A和B有交集,确保在并集中只有1份交集
list2.removeAll(list1);
list1.addAll(list2);

建议77:使用shuffle打乱列表
int tagCloudNum = 10; //云标签
List<String> tagClouds = new ArrayList<>(tagCloudNum);
Collections.shuffle(tagCloudNum);

1、用在程序伪装。
例如,标签云,游戏打怪、修行、群殴、宝物分配的策略。
2、抽奖程序
确定抽奖几率
3、安全传输
sender发送一组数据,先随机打乱,然后加密传输
receiver解密,自行排序。

建议78:减少HashMap中元素的数量
HashMap偶尔会出现溢出问题,OutOfMemory,大量的增删改查操作。
HashMap底层是数组方式保存元素,每一个键值对就是一个元素,键值对封装成Entry对象,并放到数组里面。
HashMap底层数组变量叫table是Entry类型。比List多了一次封装。底层数组扩容超过了剩余内存的容量。2倍长度的递增。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.RandomAccess;

public class Client {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
final Runtime rt = Runtime.getRuntime();

//JVM终止前记录内存信息
rt.addShutdownHook(new Thread(){
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\n");
System.out.println(sb);
}
});

for(int i = 0; i < 3932170; i++)
map.put("key" + i, "value" + i);
}
}

建议79:集合中的哈希码不要重复
List查找某值非常耗资源。
随机存取的列表是遍历查找,顺序存取的列表是链表查找,Conllections的binarySearch查找。
List.contains是遍历对比,每个元素都要查一遍。
Hash开头的集合查找最快。HashMap根据hashcode决定在数组中的位置(indexFor方法和hash方法把hashcode转换成数组的下标),hashcode()和key根据这2个值进行对比。
table数组长度是2的N次幂;元素是Entry类型;元素的位置是不连续的。
null值也可以作为key,放在数组的第一位。
hash冲突问题。通过单项链表解决。每个键值对是一个Entry,每个Entry都有一个next变量指向下一个键值对。addEntry实现。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.RandomAccess;

public class Client {
public static void main(String[] args) {
int length = 10000;
List<String> list = new ArrayList<>();
for(int i = 0; i < length; i++){
list.add("value" + i);
}

long start = System.nanoTime();

list.contains("value" + (length-1));
long end = System.nanoTime();

System.out.println("ArrayList time : " + (end - start) + " ns\n");

Map<String,String> map = new HashMap<>();
for(int i = 0; i < length ; i++)
map.put("key" + i, "value"+i);

start = System.nanoTime();
map.containsKey("key" + (length-1));
end = System.nanoTime();

3ff0
System.out.println("HashMap time : " + (end - start) + " ns\n");

}
}

建议80:多线程使用Vector或HashTable
Vector是ArrayList的多线程版本
HashTable是HashMap的多线程版本

线程安全和同步修改异常
所有集合类都有一个“快速失败”Fail-Fast的校验机制。当一个集合在多线程修改并访问时候,可能会出现一个ConcurrentModificationException异常,为了确保集合的一致性设置的保护措施。实现原理是modCount修改计数器,当modCount发生变化,就是有其它线程在修改,会抛出这个异常。
线程同步是保护集合中的数据不被脏读和脏写。
若非必要,不要使用synchronized

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Random;
import java.util.RandomAccess;
import java.util.Vector;

public class Client {
public static void main(String[] args) {
final Vector<String> tickets = new Vector<>();

for(int i = 0 ; i < 10000 ; i++)
tickets.add("火车" + i);

for(int i = 0; i < 10 ; i++){
new Thread(){
public void run(){
while(true){
System.out.println(Thread.currentThread().getId() + " " + tickets.remove(0));
}
};
}.start();
}

}
}

建议81:非稳定排序推荐使用List
TreeSet升序的Set集合,没有重复元素。TreeSet的SortedSet接口只是定义了加入元素时候进行排序,不能保证元素修改后的排序结果。所以,TreeSet适于不变元素的集合数据排序。
对于可变元素排序,2种解决办法
一个是Set集合重新排序,建立一个新的Set对象,对原有的Set重新排序。
另一个,重构掉TreeSet,用List解决。用Collections.sort()排序。解决其重复问题,转化为HashSet剔除重复元素有再转回来。

建议82:集合大家族
(1)List
ArrayList动态数组
LinkedList双向链表
Vector线程安全的动态数组
Stack对象栈,先进后出

(2)Set
Set不包含重复元素
EnumSet枚举类型的Set
HashSet以哈希码决定元素位置,快速的插入和查找
TreeSet自动排序的Set,实现了SortedSet接口

(3)Map
TreeMap,根据key值排序的Map
非排序的Map
HashMap
HashTable
Properties,是HashTable的子类,用于从Property文件中加载数据
EnumMap,key必须是枚举类型
WeakHashMap,其对象不会阻止垃圾回收器对键值的回收。

(4)Queue
阻塞式队列,队列满了以后再插入数据会抛出异常。
ArrayBlockingQueue,以数组方式实现
PriorityBlockingQueue,按照优先级
LinkedBlockingQueue,链表实现

非阻塞队列,无边界,只要内存允许,就可以追加元素
PriorityQueue

双端队列,支持在头、尾两端插入和移除数据
ArrayDeque
LinkedBlockingDeque
LinkedList

(5)数组
数组可以放基本类型,集合不行,集合底层存储都是数组。

(6)工具类
数组工具类
java.util.Arrays
java.lang.reflect.Array

集合工具类
java.util.Collections

(7)扩展类
Apache的commons-collecitons
Google的google-collections
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: