package com.cheng.improve151suggest;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.RandomAccess;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import com.cheng.highqualitycodestudy.R;
import com.cheng.improve151suggest.adapter.I151SuggestListAdapter;
import com.cheng.improve151suggest.model.City;
/**
第5章 数组和集合
建议60: 性能考虑,数组是首选
建议61: 若有必要,使用变长数组
建议62: 警惕数组的浅拷贝
建议63: 在明确的场景下,为集合指定初始容量
建议64: 多种最值算法,适时选择
建议65: 避开基本类型数组转换列表陷阱
建议66: aslist方法产生的list对象不可更改
建议67: 不同的列表选择不同的遍历方法
建议68: 频繁插入和删除时使用linkedlist
建议69: 列表相等只需关心元素数据
建议70:子列表只是原列表的一个视图
建议71: 推荐使用sublist处理局部列表
建议72: 生成子列表后不要再操作原列表
建议73: 使用comparator进行排序
建议74: 不推荐使用binarysearch对列表进行检索
建议75: 集合中的元素必须做到compareto和equals同步
建议76: 集合运算时使用更优雅的方式
建议77: 使用shuffle打乱列表
建议78: 减少hashmap中元素的数量
建议79: 集合中的哈希码不要重复
建议80: 多线程使用vector或hashtable
建议81: 非稳定排序推荐使用list
建议82: 由点及面,一叶知秋—集合大家族
*/
public class I151SChapter05Activity extends AppCompatActivity {
private static final String TAG = "I151SChapter05Activity";
private ListView mChapterLV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_i151suggestchapter);
initView();
initData();
}
private void initView() {
this.mChapterLV = (ListView) findViewById(R.id.isi_chapter_lv);
}
private void initData() {
String[] suggests = getResources().getStringArray(R.array.i151schapter5);
I151SuggestListAdapter adapter = new I151SuggestListAdapter(this, Arrays.asList(suggests));
mChapterLV.setAdapter(adapter);
}
/**
* 建议60: 性能考虑,数组是首选
*/
private void suggest60() {
/**
* 注意
* 性能要求较高的场景使用数组替代集合
*/
}
/**
* 建议61: 若有必要,使用变长数组
*/
private void suggest61() {
// 一个班级最多容纳60个学生
Integer[] classes = new Integer[60];
/*classes初始化...*/
// 偶尔一个班级可以容纳80人,数组加长
classes = expandCapacity(classes, 80);
/*重新初始化超过限额的20人...*/
/**
* 注意
* 通过这样的处理方式,曲折地解决了数组的变长问题。其实,集合的长度自动维护功能的原理与此类似。
* 在实际开发中,如果确实需要变长的数据集,数组也是在考虑范围之内的,不能因固定长度而将其否定
*/
}
// 采用Arrays数组工具类的copyOf方法,产生一个newLen长度的新数组,并把原有的值拷贝了进去,之后就可以
// 对超长的元素进行赋值了
private <T> T[] expandCapacity(T[] datas, int newLen) {
// 不能是负值
newLen = newLen<0 ? 0: newLen;
return Arrays.copyOf(datas, newLen);
}
/**
* 建议62: 警惕数组的浅拷贝
*/
private void suggest62() {
/**
* 注意
* 通过copyOf方法产生的一个数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,
* 其他都是拷贝引用地址。需要说明的是,数组的clone方法也是与此相同的,同样是浅拷贝,而且集合的
* clone方法也都是浅拷贝,在拷贝时需要多留心
*/
}
/**
* 建议63: 在明确的场景下,为集合指定初始容量
*/
private void suggest63() {
/*
查看ArrayList源码,可以看出,如果不设置初始容量,系统就按照1.5倍的规则扩容,每次扩容都是一次数组
的拷贝,如果数据量很大,这样的拷贝会非常耗费资源,而且效率非常低下。如果已经知道一个ArrayList的可
能长度,然后对ArrayList设置一个初始容量则可以显著提高系统性能。比如一个班级的学生,通常也就是50人
左右,就声明ArrayList的默认容量为50的1.5倍(元素数量小,直接计算,避免数组拷贝),即new ArrayList
<Student>(75);这样在使用add方法增加元素时,只要在75以内都不用做数组拷贝,超过了75才会按照默认规则
扩容。如此处理,对开发逻辑并不会有任何影响,而且可以提高运行效率
*/
/**
* 注意
* 非常有必要在集合初始化时声明容量
*/
}
/**
* 建议64: 多种最值算法,适时选择
*/
private void suggest64() {
Integer[] datas = new Integer[]{};
// 转换为列表
List<Integer> dataList = Arrays.asList(datas.clone());
// 转换为TreeSet,删除重复元素并升序排列
TreeSet<Integer> treeSet = new TreeSet<>(dataList);
// 取得比最大值小的最大值,也就是老二了
Integer second = treeSet.lower(treeSet.last());
/*
剔除重复元素并升序排列,这都由TreeSet类实现的,然后可再使用lower方法寻找小于最大值的值
*/
/**
* 注意
* 最值计算时使用集合最简单,使用数组性能最优
*/
}
/**
* 建议65: 避开基本类型数组转换列表陷阱
*/
private void suggest65() {
int[] data = {1,2,3,4,5};
List list = Arrays.asList(data);
Log.e(TAG, "列表中的元素数量是:" + list.size()); // 1
Log.e(TAG, "元素类型:" + list.get(0).getClass()); // class [I
Log.e(TAG, "转换前后是否相等:" + data.equals(list.get(0)));
// 代码修正如下
Integer[] data2 = {1,2,3,4,5};
List list2 = Arrays.asList(data2);
Log.e(TAG, "列表中的元素数量是:" + list2.size());
/*
仔细看一下Arrays.asList的方法说明:输入一个变长参数,返回一个固定长度的列表。注意这里是一个变
长参数,看源代码:
public static <T> List<T> asList(T... a) {
return new ArrayList<T<(a);
}
asList方法输入的是一个泛型变长参数,我们知道基本类型是不能泛型化的,也就是说8个基本类型不能作为
泛型参数,要想作为泛型参数必须使用其所对应的包装类型。那上面的例子传递了一个int类型的数组,为什么
程序没有报编译错呢?
在Java中,数组是一个对象,它是可以泛型化的,也就是说上面的例子是把一个int类型的数组做为了T的类型,
所以转换后在List中就只有一个类型为int数组的元素了
可看到上面打印输出:“元素类型:class [I”?我们并没有指明是数组(Array)类型呀!这是因为JVM不可能
输出Array类型,因为Array是属于java.lang.reflect包的,它是通过反射访问数组元素的工具类。在Java
中任何一个数组的类型都是“[I”,究其原因就是Java并没有定义数组这一个类,它是在编译器编译的时候生成
的,是一个特殊的类,在JDK的帮助中也没有任何数组类的信息
*/
/**
* 注意
* 原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱
*/
}
/**
* 建议66: aslist方法产生的list对象不可更改
*/
enum Week {Sun, Mon, Tue, Wed, Thu, Fir, Sat}
private void suggest66() {
// 五天工作制
Week[] workDays = {Week.Mon, Week.Tue, Week.Wed, Week.Thu, Week.Fir};
// 转换为列表
List<Week> list = Arrays.asList(workDays);
// 增加周六也为工作日
list.add(Week.Sat); // UnsuppostedOperationException
/* 工作日开始干活了 */
/*
asList()返回的List居然不支持add方法,这真是奇怪了!来看看asList方法的源代码:
public static <T> List<T> asList(T... a) {
return new ArrayList<T<(a);
}
直接new了一个ArrayList对象返回,难道ArrayList不支持add方法?不可能呀!可能,问题就出在这个
ArrayList类上,此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类(私有静态内部类),
除了Arrays能访问外,其他类都不能访问。再深入地看看这个ArrayList静态内部类,它仅仅实现了5个方法:
size:元素数量
toArray:转化为数组,实现了数组的浅拷贝
get:获取指定元素
set:重置某一元素
contains:是否包含某元素
对于常用的add和remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表
*/
/**
* 注意
* 有些开发者喜欢通过如下方式定义和初始化列表:
* List<Stirng> names = Arrays.asList("Java", "Android", "OC");
* 一句话完成了列表的定义和初始化,看似很便捷,却深藏着重大隐患--列表长度无法修改
* 如果有这种习惯,请慎之戒之,除非非常自信该List只用于读操作
*/
}
/**
* 建议67: 不同的列表选择不同的遍历方法
*/
private void suggest67() {
/*
ArrayList数组实现了RandomAccess接口(随机存取接口),这也就标志着ArrayList是一个可以随机存取的
列表。在Java中,RandomAccess和Cloneable、Serializable一样,都是标志性接口,不需要任何实现,只
是用来表明其实现类具有某种特质的,实现了Cloneable表明可以被拷贝,实现了Serializable接口表明被序
列化了,实现了RandomAccess则表明这个类可以随机存取,对ArrayList来说也就是标志着其数据元素之间没
有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储
Java中的foreach语法是Iterator(迭代器)的变形用法,如下代码是等价的:
int sum = 0;
for(int i : list) {
sum += i;
}
等价于
for(Iterator<Integer> i=list.iterator; i.hasNext(); ) {
sum += i;
}
那想想什么是迭代器,迭代器是23种设计模式中的一种,“提供一种方法访问一个容器对象中的各个元素,同时
又无须暴露该对象的内部细节”,也就是对于ArrayList,需要先创建一个迭代器容器,然后屏蔽内部遍历细节,
对外提供hasNext、next等方法。问题是ArrayList实现了RandomAccess接口,已表明元素之间本来没有关系,
可以,为了使用迭代器就需要强制建立一种互相“知晓”的关系,比如上一个元素可以判断是否有下一个元素,以
及下一个元素是什么等关系,这也是通过foreach遍历耗时的原因
*/
// 优化的遍历求和
int sum = 0;
List<Integer> list = null;
if (list instanceof RandomAccess) {
// 可以随机存取,则使用下标遍历
for (int i=0,size=list.size(); i < size; i++) {
sum += list.get(i);
}
} else {
// 有序存放,使用foreach方式
for (int i : list) {
sum += i;
}
}
/**
* 注意
* 列表遍历不是那么简单的,其中很有“学问”,适时选择最优的遍历方式,不要固化为一种
*/
}
/**
* 建议68: 频繁插入和删除时使用linkedlist
*/
private void suggest68() {
/**
* 注意
* LindedList在插入和删除元素时表现好
* ArrayList在读取和修改元素时效率高
*/
}
/**
* 建议69: 列表相等只需关心元素数据
*/
private void suggest69() {
ArrayList<String> strs = new ArrayList<>();
strs.add("A");
Vector<String> strs2 = new Vector<>();
strs2.add("A");
Log.e(TAG, "strs.equals(strs2)" + strs.equals(strs2)); // true
/**
* 注意
* 判读集合是否相等时只须关注元素是否相等即可
*/
}
/**
* 建议70:子列表只是原列表的一个视图
*/
private void suggest70() {
// 定义一个包含两个字符串的列表
List<String> c = new ArrayList<>();
c.add("A");
c.add("B");
// 构造一个包含c列表的字符串列表
List<String> c1 = new ArrayList<>(c);
// subList生成与c相同的列表
List<String> c2 = c.subList(0, c.size());
// c2增加一个元素
c2.add("C");
Log.e(TAG, "c == c1 ? " + c.equals(c1)); // false
Log.e(TAG, "c == c2 ? " + c.equals(c2)); // true
// String也有个substring方法,看看它是如何工作的
String str = "AB";
String str1 = new String(str);
String str2 = str.substring(0) + "C";
Log.e(TAG, "str == str1 ? " + str.equals(str1));
Log.e(TAG, "str == str2 ? " + str.equals(str2));
/*
看上面的例子,与String类刚好相反,同样是一个sub类型的操作,为什么会相反呢?
查看subList的源代码,它的实现原理是这样的:它返回的SubList类也是AbstractList的子类,其所有的方
法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列
表这是原列表的一个视图(View),所有的修改动作都反映在原列表上。解释了c==c2,再回过头来看看为什么
变量c与c1不相等。很简单,因为通过ArrayList构造函数创建的List对象c1实际上是新列表,它是通过数组的
copyOf动作生成的,所生成的列表c1与原列表c之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就
是说元素是深拷贝的),然后c又增加了元素,因为c1与c之间已经没有一毛钱关系了,那自然是不相等了
*/
/**
* 注意
* subList产生的列表只是一个视图,所有的修改动作直接作用于原列表
*/
}
/**
* 建议71: 推荐使用sublist处理局部列表
*/
private void suggest71() {
/*
来看这样一个简单的需求:一个列表有100个元素,现在要删除索引位置为20~30的元素
*/
// 方案1 一个遍历很快就可以完成
// 初始化一个固定长度,不可变列表
List<Integer> initData = Collections.nCopies(100, 0);
// 转换为可变列表
List<Integer> list = new ArrayList<>(initData);
// 遍历,删除符合条件的元素
for (int i=0, size=list.size(); i < size; i++) {
if (i>=20 && i<30) {
list.remove(i);
}
}
// 或者
for (int i = 20; i < 30; i++) {
if (i < list.size()) {
list.remove(i);
}
}
// 遍历一遍,符合条件的就删除,简单而又实用。不过,还有没有其他方式呢?有没有“one-lining”一行代码
// 就解决问题的方式呢?
// 有,直接使用ArrayList的removeRange方法不就可以了吗?等等,这个方法有protected关键字修饰着,不
// 能直接使用,那怎么办?看如下代码:
// 方案2
// 初始化一个固定长度,不可变列表
List<Integer> initData2 = Collections.nCopies(100, 0);
// 转换为可变列表
List<Integer> list2 = new ArrayList<>(initData2);
// 删除指定范围的元素
list.subList(20, 30).clear();
/**
* 注意
* 因为subList返回的List是原始列表的一个视图,删除这个视图中的所有元素,最终就会反映到原始字符串上,
* 那么一行代码即解决问题了
*/
}
/**
* 建议72: 生成子列表后不要再操作原列表
*/
private void suggest72() {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList = list.subList(0, 2);
// 原字符串增加一个元素
list.add("D");
Log.e(TAG, "原列表长度:" + list.size());
Log.e(TAG, "子列表长度:" + subList.size()); // 抛出异常 ConcurrentModificationException
/*
这里根本就没有多线程操作,何来并发修改呢?这个问题很容易回答,那是因为subList取出的列表是原列表
的一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表(
这点与数据库视图是不同的),后面再对子列表继续操作时,就会检测到修改计数器与预期的不相同,于是就
抛出了并发修改异常。出现这个问题的最终原因还是在子列表提供的size方法的检查上(可看size方法源码)
subList的其他方法也会检测修改计数器,例如set、get、add等方法,若生成子列表后,再修改原列表,这
些方法也会抛出ConcurrentModificationException异常
对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致“视图”的不稳定,最有效
的办法就是通过Collections.unmodifiableList方法设置列表为只读状态,代码如下:
*/
List<String> list2 = new ArrayList<>();
List<String> subList2 = list.subList(0, 2);
// 设置列表为只读状态
list2 = Collections.unmodifiableList(list2);
// 对list2进行只读操作
// doReadSomething(list2);
// 对subList2进行读写操作
// doReadAndWriteSomething(subList2);
/*
还有一个问题,数据库的一张表可以有很多视图,我们的List也可以有多个视图,也就是可以有多个子列表,
但问题是只要生成的子列表多于一个,则任何一个子列表就都不能修改了,否则就会抛出并发修改异常
*/
/**
* 注意
* subList生成子列表后,保持原列表的只读状态
*/
}
/**
* 建议73: 使用Comparator进行排序
*/
private void suggest73() {
/*
在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是实现Comparator接口,
这两者有什么区别呢?
实现了Comparable接口的类表明自身是可比较的,有了比较才能进行排序;而Comparator接口是一个工具类接
口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比
较逻辑,从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生
N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法
基本不会改变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法
*/
/**
* 注意
* Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具
*/
}
/**
* 建议74: 不推荐使用binarysearch对列表进行检索
*/
private void suggest74() {
List<String> cities = new ArrayList<>();
cities.add("上海");
cities.add("广州");
cities.add("广州");
cities.add("北京");
cities.add("天津");
// indexOf方法取得索引值
int index1 = cities.indexOf("广州");
// binarySearch查找到索引值
int index2 = Collections.binarySearch(cities, "广州");
Log.e(TAG, "索引值(indexOf):" + index1); // 1
Log.e(TAG, "索引值(binarySearch):" + index1); // 2
/*
结果不一样,查看源码,两者的算法都没有问题,难道是用错了?这还真是使用错了,因为二分法查询的一个首
要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。不排序怎么确定是在小区(比中间值小
的区域)中查找还是在大区(比中间值大的区域)中查找呢?二分法必须要先排序,这是二分法查找的首要条件
*/
/**
* 使用binarySearch首先要考虑排序问题,从性能方面考虑,binarySearch是最好的选择
*/
}
/**
* 建议75: 集合中的元素必须做到compareto和equals同步
*/
private void suggest75() {
List<City> cities = new ArrayList<City>();
cities.add(new City("021", "上海"));
cities.add(new City("021", "沪"));
City city = new City("021","沪");
//排序
Collections.sort(cities);
// indexOf方法取得索引值
int index1 = cities.indexOf(city);
// binarySearch查找到索引值
int index2 = Collections.binarySearch(cities, city);
Log.e(TAG, "索引值(indexOf):" + index1); // 0
Log.e(TAG, "索引值(binarySearch):" + index2); // 1
/*
indexOf返回的是第一个元素,而binarySearch返回的是第二个元素(索引值是1),这是怎么回事呢?
这是因为indexOf是通过equals方法判断的,equals等于true就认为找到符合条件的元素了,而binarySearch
查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素
仔细审查一下代码,City类覆写了compareTo和equals方法,但是两者并不一致
从这个例子中,可以理解两点:
indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找
equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同
既然一个是决定排序位置,一个是决定相等,那就应该保证当排序位置相同时,其equals也相同,否则就会产生
逻辑混乱
*/
/**
* 注意
* 实现了compareTo方法,就应该覆写equals方法,确保两者同步
*/
}
/**
* 建议76: 集合运算时使用更优雅的方式
*/
private void suggest76() {
// 遍历可以实现并集、交集、差集等运算,但这不是最优雅的处理方式,看看如何进行更优雅的集合操作
// 1)并集 也叫做合集,把两个集合加起来即可
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
List<String> list2 = new ArrayList<>();
list2.add("C");
list2.add("B");
// 并集
list1.addAll(list2);
// 2)交集 计算两个集合的共有元素,也就是你有我也有的元素集合
list1.retainAll(list2);
// 注意retainAll方法会删除list1中没有出现在list2中的元素
// 3)差集 由所有属于A但不属于B的元素组成的集合,叫做A与B的差集
list1.removeAll(list2);
// 从list1中删除出现在list2的元素,即可得出list1与list2的差集部分
// 4)无重复的并集 并集是集合A加集合B,那如果集合A和集合B有交集,就需要确保并集的结果中只有一份交集,
//此为无重复的并集
// 删除在list1中出现的元素
list2.removeAll(list1);
// 把剩余的list2元素加到list1中
list1.addAll(list2);
/**
* 注意
* 集合的这些操作在持久层中使用得非常频繁,从数据库中取出的就是多个数据集合,之后我们就可以使用集合
* 的各种方法构建我们需要的数据了,需要两个集合的and结果,那是交集,需要两个集合的or结果,那是并集,
* 需要两个集合的not结果,那是差集
*/
}
/**
* 建议77: 使用shuffle打乱列表
*/
private void suggest77() {
/**
* 注意
* 一般很少用到shuffle这个方法,那它可以用在什么地方呢?
* 可以用在程序的“伪装”上
* 比如标签云,或者是游戏中的打怪、修行、群殴时宝物的分配策略
* 可以用在抽奖程序中
* 可以用在安全传输方面
* 比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后自行排序,即可实现即使是
* 相同的数据源,也会产生不同密文的效果,加强了数据的安全性
*/
}
/**
* 建议78: 减少HashMap中元素的数量
*/
private void suggest78() {
Map<String, String> map = new HashMap<>();
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");
Log.e(TAG, sb.toString());
}
});
// 放入近40万键值对
for (int i = 0; i < 393217; i++) {
map.put("key" + i, "value" + i);
}
/*
该程序只是向Map中放入了近40万个键值对(不是整40碗个,而是393217个,为什么呢?),只是增加,没有
任何其他操作。想想看,会出现什么问题?内存溢出?
运行发现,内存溢出了!难道是String字符串太多了?不对呀,字符串对象加起来撑死也就10MB。或者是put
方法有缺陷,产生了内存泄漏?这里还有可用内存,应该要用尽了才会出现内存泄漏啊
为了更清晰地理解该问题,与ArrayList做一个对比,把相同数据插入到ArrayList中看看会怎样,代码如下:
*/
List<String> list = new ArrayList<>();
/*Runtime增加的钩子函数相同*/
// 放入40万相同字符串
for (int i = 0; i < 400000; i++) {
list.add("key" + i);
list.add("value" + i);
}
/*
ArrayList运行很正常,没有出现内存溢出情况。两个容器,容纳的元素相同,数量相同,ArrayList没有溢出,
但HashMap却溢出了。很明显,这与HashMap内部的处理机制有极大的关系
HashMap在底层也是以数组方式保存元素的,其中每一个键值对就是一个元素,也就是说HashMap把键值对封装
成一个Entry对象,然后再把Entry放到数组中
回过头来想想,对上面的例子来说,HashMap比ArrayList多了一次封装,把Stirng类型的键值对转换成Entry
对象后再放入数组,这就多了40万个对象,这应该是问题产生的第一个原因
HashMap的长度也是可以动态增加的,它的扩容机制与ArrayList稍有不同。在Map的size为393216时,符合了
扩容条件,于是393216个元素准备开始大搬家,要扩容嘛,那首先要申请一个长度为1048576(当前长度的两倍
嘛,2的19次方再乘以2,即2的20次方)的数组,但是问题是此时剩余的内存不足以支撑此运算,于是就报内存
溢出了!这是第二个原因,也是最根本的原因
综合来说,HashMap比ArrayList多了一个层Entry的底层对象封装,多占用了内存,并且它的扩容策略是2倍长
度的递增,同时还会依据阈值判断规则进行判断,因此相对于ArrayList来说,它就会先出现内存溢出
*/
/**
* 注意
* 尽量让HashMap中的元素少量并简单
*/
}
/**
* 建议79: 集合中的哈希码不要重复
*/
private void suggest79() {
int size = 10000;
List<String> list = new ArrayList<>(size);
// 初始化
for (int i = 0; i < size; i++) {
list.add("value" + i);
}
// 记录开始时间,单位纳秒
long start = System.nanoTime();
// 开始查找
list.contains("value" + (size - 1));
// 记录结束时间,单位纳秒
long end = System.nanoTime();
Log.e(TAG, "list执行时间" + (end - start) + "ns");
// Map的查找时间
Map<String, String> map = new HashMap<>(size);
for (int i = 0; i < size; i++) {
map.put("key" + i, "value" + i);
}
start = System.nanoTime();
map.containsKey("key" + (size - 1));
end = System.nanoTime();
Log.e(TAG, "map执行时间" + (end - start) + "ns");
/*
HashMap的效率比ArrayList高很多
HashMap每次增加元素时都会先计算其哈希吗,然后使用hash方法再次对hashCode进行抽取和统计,同时兼顾哈
希码的高位和低位信息产生一个唯一值,也就是说hashCode不同,hash方法返回的值也不同。之后再通过indexFor
方法与数组长度做一次与运算,即可计算出其在数组中的位置,简单地说,hash方法和indexFor方法就是把哈希
码转变成数组的下标
顺便说明一下,null值也是可以做为key值的,它的位置永远在Entry数组中的第一位
关于哈希冲突(两个不同的Entry,可能产生相同的哈希码),HashMap是如何处理这种冲突问题的呢?答案是通
过链表,每个键值对都是一个Entry,其中每个Entry都有一个next变量,也就是说它会指向下一个键值对。如果
新加入的键值对的hashCode是唯一的,那直接插入的数组中,它Entry的next值则为null;如果新加入的键值对
的hashCode与其他元素冲突,则替换掉数组中的当前值,并把新加入的Entry的next变量指向被替换掉的元素--
于是,一个链表就生成了
HashMap的存储主线还是数组,遇到哈希冲突的时候则使用链表解决。了解了HashMap是如何存储的,查找也就一
目了然了:使用hashCode定位元素,若有哈希冲突,则遍历对比,换句话说在没有哈希冲突的情况下,HashMap的
查找则是依赖hashCode定位的,因为是直接定位,那效率当然就高了
*/
/**
* 注意
* HashMap中的hashCode应该避免冲突
*/
}
/**
* 建议80: 多线程使用vector或hashtable
*/
private void suggest80() {
// suggest80 Code1
testSuggest80Code1(); // 运行,抛出ConcurrentModificationException
/*
把ArrayList替换成Vector试试,结果照旧,抛出相同的异常,Vector已经是线程安全的,为什么还报这个错误?
因为这里混淆了线程安全和同步修改异常,基本上所有的集合类都有一个叫做快速失败(Fail-Fast)的校验机制,
当一个集合在被多个线程修改并访问时,就可能会出现ConcurrentModificationException异常,这是为了确保
集合方法一致而设置的保护措施,它的实现原理是我们经常提到的modCount修改计数器:如果在读列表时,
modCount发生变化(也就是有其他线程修改)则会抛出ConcurrentModificationException异常。这与线程同步
是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的,来看看线程安全用在什么地方,看Code2
*/
testSuggest80Code2(); // 运行,会发现有多个线程在卖同一张票
/*
注意,上面有两个线程在卖同一张火车票,这才是线程不同步的问题,此时把ArrayList修改为Verctor即
可解决问题,因为Verctor的每个方法前都加上了synchronized关键字,同时只会允许一个线程进入该方法,
确保了程序的可靠性
*/
/**
* 注意
* 多线程环境下考虑使用Vector或HashTable
* 这里说的是真正的多线程,不是并发修改的问题,比如一个线程增加,一个线程删除,这不属于多线程的范畴
*/
}
/**
* 建议81: 非稳定排序推荐使用list
*/
private void suggest81() {
//==========1==========//
SortedSet<Person> set1 = new TreeSet<>();
// 身高180CM
set1.add(new Person(180));
// 身高175CM
set1.add(new Person(175));
for(Person p:set1){
Log.e(TAG, "身高:"+p.getHeight());
}
//==========2==========//
SortedSet<Person> set2 = new TreeSet<Person>();
// 身高180CM
set2.add(new Person(180));
// 身高175CM
set2.add(new Person(175));
// 身高最矮的人大变身
set2.first().setHeight(185);
for (Person p : set2) {
Log.e(TAG, "身高:" + p.getHeight());
}
/*
运行发现,竟然没有重新排序
SortedSet接口(TreeSet实现了该接口)只是定义了在该集合加入元素时将其进行排序,并不能保证元素
修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String、Integer等类型,但不适合
用于可变量的排序,特别是不确定何时元素会发生变化的数据集合
*/
//==========3==========//
SortedSet<Person> set3 = new TreeSet<Person>();
// 身高180CM
set3.add(new Person(180));
// 身高175CM
set3.add(new Person(175));
// 身高最矮的人大变身
set3.first().setHeight(185);
//set重排序
set3 = new TreeSet<Person>(new ArrayList<Person>(set3));
for (Person p : set3) {
Log.e(TAG, "身高:" + p.getHeight());
}
/*
上的代码使用Set集合重排序,使数据变化后重新变得有序
*/
//==========4==========//
List<Person> list4 = new ArrayList<Person>();
// 身高180CM
list4.add(new Person(180));
// 身高175CM
list4.add(new Person(175));
// 身高最矮的人大变身
list4.get(1).setHeight(185);
//排序
Collections.sort(list4);
for (Person p : list4) {
Log.e(TAG, "身高:" + p.getHeight());
}
/*
上面代码彻底重构掉TreeSet,使用List,解决数据变化后的排序问题
*/
/**
* 注意
* SortedSet中的元素被修改后可能会影响其排序位置
*/
}
/**
* 建议82: 由点及面,一叶知秋——集合大家族
*/
private void suggest82() {
/*
1)List
实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一个动态
数组,LinkedList是一个双向链表,Vector是一个线程安全的的动态数组,Stack是一个对象栈(FILO)
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是静悄悄回收的
4)Queue
队列,它分为两类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括:
ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue
是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组建的队列,
LinkedBlockingQueue是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可
以持续追加元素
还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDeque、
LinkedBlickingDeque、LinkedList
5)数组
数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行,更重要的一点就是所有的集合底层存储的
都是数组
6)工具类
数组的工具类是java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections,
有了这两个工具类,操作数组和集合会易如反掌,得心应手
7)扩展类
可以使用Apache的commons-collections扩展包,也可以使用Google的google-collections扩展包
*/
/**
* 注意
* commons-collections、google-collections是JDK之外的优秀数据集合工具包
*/
}
private void testSuggest80Code1() {
// 火车票列表
final List<String> tickets = new ArrayList<>();
// 初始化票据池
for (int i = 0; i < 100000; i++) {
tickets.add("火车票" + i);
}
// 退票
Thread returnThread = new Thread(){
@Override
public void run() {
while (true) {
tickets.add("车票" + new Random().nextInt());
}
}
};
// 售票
Thread saleThread = new Thread() {
@Override
public void run() {
for (String ticket : tickets) {
tickets.remove(ticket);
}
}
};
// 启动退票线程
returnThread.start();
// 启动售票线程
saleThread.start();
}
private void testSuggest80Code2() {
// 火车票列表
final List<String> tickets = new ArrayList<>();
// 初始化票据池
for (int i = 0; i < 100000; i++) {
tickets.add("火车票" + i);
}
// 10个窗口售票
for (int i = 0; i < 10; i++) {
new Thread(){
@Override
public void run() {
while (true) {
Log.e(TAG, Thread.currentThread().getName() +
":" + Thread.currentThread().getId() +
"----" + tickets.remove(0));
}
}
}.start();
}
}
static class Person implements Comparable<Person>{
//身高
private int height;
public Person(int _age){
height = _age;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
//按照身高排序
public int compareTo(Person o) {
return height - o.height;
}
}
}