Map集合
都是双列集合,存放 K-V
接口特点
注:1. Set本来也是 Key - Value 结构,但是它的Value一直都是PRESENT,因此可以看作Key。
2. 虽然key不允许重复,但是如果重复添加会导致覆盖。
3. 用 put 方法输入,用 get 方法指定key返回value。
内部创建EntrySet
k-v 最后是 HashMap$Node node = newNode(hash,key,value,null)。注意 k-v 为了方便程序员的遍历,还会在内部创建 EntrySet 集合,KeySet集合以及Values集合。其中KeySet里面存放key,Values(当然只是引用(地址)!不是真正的结点,真正的结点在Node中)。而EntrySet则是包括KeySet与Values,里面内容的运行类型为Node。
entrySet中,存放的元素的类型为Map.Entry<k,v>(entrySet相当于一个数组)。真正存放k-v的地方还是 HashMap$Node。
为什么能够转型:因为 Node类实现了Entry接口。当把 Node对象存放到 entrySet 就方便我们的遍历,因为 Map.Entry 提供了重要的方法 getKey 以及 getValue。
如果只想使用key,那么就用KeySet。如果只想用value,那么用values。如果两者都想用,那么就用EntrySet。
public static void main(String[] args) {
Map a = new HashMap();
a.put("张三",18); //put方法加入键值对
a.put("李四",20);
a.put("王五",21);
Set b = a.entrySet(); // 用entrySet遍历,entrySet父类是Set,因此用Set接收
for (Object obj : b) {
Map.Entry t = (Map.Entry)obj; //向下转型,编译类型为 Map.Entry
System.out.println(t.getKey()+" " + t.getValue());
}
Set c = a.keySet(); //只能操作key
Collection d = a.values(); //只能操作value
}
基本方法
public static void main(String[] args) {
Map a = new HashMap();
a.put("张三",18);
a.put("李四",9);
a.put("王五",31);
a.put("尚",20);
System.out.println(a);//{李四=9, 张三=18, 王五=31, 尚=20}
a.remove("李四"); //根据某个Key删除结点
System.out.println(a);//{张三=18, 王五=31, 尚=20}
System.out.println(a.get("张三"));//18 根据Key返回value
System.out.println(a.size());//3
System.out.println(a.isEmpty());//false
System.out.println(a.containsKey("王五"));//true 查找是否有这个Key
a.clear(); //清除
System.out.println(a.isEmpty());//true
}
注:如果要修改元素的内容,也可以用put,因为可以覆盖。 P550
遍历方法
会在泛型里进一步改写
用keySet
public static void main(String[] args) {
Map map = new HashMap(); //这里可以用泛型改写
map.put("张三",18);
map.put("李四",9);
map.put("王五",31);
map.put("尚",20);
Set set = map.keySet(); //获取所有的key
//第一种方法 增强for循环
for (Object key : set) {
System.out.println(key + "->" + map.get(key));//用get方法获取value
}
//第二种方法 迭代器
Iterator iterator = set.iterator(); //获取set对象的迭代器
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "->" + map.get(key));
}
}
用values
Collection values = map.values(); //获取values
for (Object key : values) { //增强for
System.out.println(key); //注意 Map中没有从value获取key的方法
}
Iterator iterator = values.iterator(); //迭代器
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key);
}
用EntrySet
Set set = map.entrySet(); // entrySet是Set的子类,是Map的内部类
//1. 增强for
for (Object entry : set) {
// entry是Object类,需要向下转型(如果使用了泛型就不需要了)
Map.Entry t = (Map.Entry)entry;
System.out.println(t.getKey() + "->" + t.getValue());
}
//2. 迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry h = (Map.Entry)iterator.next(); //一步到位写法
System.out.println(h.getKey() + "->" + h.getValue());
}
P534 value为对象需要调用方法的情况
for (Object o : s) {
Map.Entry t = (Map.Entry)o;
if(((Employee)t.getValue()).getSal()>18000) //注意方法前要加括号,作为一个整体
if((Employee)t.getValue().getSal()>18000) //这样就是错的
...
}
for (Object o : s) { //可读性更强
Map.Entry t = (Map.Entry)o;
Employee ee = (Employee) t;
if(ee.getSal()>18000)
}
HashMap底层
HashMap底层源码
因为HashSet底层就是HashMap,因此底层几乎完全相同。
1. 执行构造器 new HashMap(),初始化加载因子 loadfactor = 0.75,HashMap$Node[] table = null。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
transient Node<K,V>[] table;
2. 执行put,调用hash方法,计算key的hash值(注意要进入一个方法最好使用 force step into)。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3. 执行putVal 方法,具体注释已经在 HashSet中给出。
HashTable
扩容
底层有数组 Hashtable$Entry[],初始化大小为 11。临界值 threshold 为8 = 11 * 0.75。
调用put方法里的 addEntry(hash, key, value, index); 当满足 if (count >= threshold) 扩容(rehash)
int newCapacity = (oldCapacity << 1) + 1; //新容量计算方法
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 临界值计算方法
Properties
Java 读写Properties配置文件 - 旭东的博客 - 博客园 感兴趣可以看这篇文章。
注意事项
1. Properties 继承 Hashtable,是无序的。
2. 可以通过 k-v 存放数据,当然key和value不能为null。
3. 常用方法:增 put(key,value),删 remove(key),改 put(相同的key,value),查 get(key)。
开发中如何选择集合实现类
一组对象指的就是只有key,没有value。
TreeSet
构造方法
正常的TreeSet声明应该是这样的:
TreeSet a = new TreeSet();
但是TreeSet有一个构造器,可以传入一个比较器Comparator(匿名内部类)
public static void main(String[] args) {
TreeSet a = new TreeSet(new Comparator() { //匿名内部类
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String)o2); //按照字符串大小比较
}
});
a.add("jack");
a.add("a");
a.add("sss");
a.add("mmm");
System.out.println(a);// [a, jack, mmm, sss]
}
要注意一个问题:假设要求按照字符串的length来从小到大排序
public static void main(String[] args) {
TreeSet a = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length() - ((String)o2).length(); //从小到大
}
});
a.add("jack");
a.add("a");
a.add("sss");
a.add("mmm");
System.out.println(a); // [a, sss, jack]
}
可以发现 "mmm" 并没有加入进去,那么我们就需要追一下源码了。
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
} // 初始化,生成一个新结点
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator; //重点在这里,把比较器赋过去
if (cpr != null) {
do { //对整个链表(key)进行循环,给当前key找适当位置
parent = t;
cmp = cpr.compare(key, t.key); //比较原结点与要加入的结点的key
//这里会动态绑定到匿名内部类对象
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right; //按照比较结果移动指针
else //遍历过程中发现准备添加的key和当前已有的key相等
return t.setValue(value); //由于Set的value为PRESENT,因此相当于没加
} while (t != null); //循环结束后,t就指向结点应该加入的位置
//parent为上一次,因此结束后还需要再移动一次
}
... //没有比较器的情况
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e; //按照比较结果把结点e放在正确位置(parent是原来的t)
fixAfterInsertion(e);
size++; //结点个数加一
modCount++; //修改次数加一
return null;
}
"sss" 是先加入的,长度为3。因为自定义的比较器是比较长度的,而 "mmm" 的长度也为3,因此结果为0,直接不加入了(参考do里面的else情况)。
由于TreeSet的底层是TreeMap,因此比较器初始化方法在TreeMap里。
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
TreeSet和TreeMap的底层都是TreeMap,因此TreeMap的源码不再做解析。
关于TreeSet加入自定义类
如果TreeSet没有重写Comparator,并且加入的类也没有实现Comparable接口,那么就会报错,因为add源码里需要赋予一个比较器。
TreeSet a = new TreeSet();
a.add(1); //这样是没有问题的,因为1相当于Integer,而Integer实现了Comparable接口
public static void main(String[] args){
TreeSet a = new TreeSet();
a.add(new Car("AAA",2331313)); //报错 ClassCastException
}
class Car{
String name;
double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
}
TreeSet a = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
}); //这样写就没问题了
class Car implements Comparable{ //这样写也没问题,Car类实现了Comparable接口
String name;
double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
@Override //重写compareTo方法
public int compareTo(Object o) {
return 0;
}
}
Collections
方法(均为静态)
public static void main(String[] args) {
List a = new LinkedList();
a.add("Tom");
a.add("king");
a.add("milan");
Collections.reverse(a);
System.out.println(a); //[milan, king, Tom]
Collections.shuffle(a);
System.out.println(a); //[Tom, king, milan]
Collections.sort(a);
System.out.println(a);//[Tom, king, milan] (m>k>T) 用字符串的大小比较
Collections.sort(a, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length()-((String)o2).length(); //按照长度从小到大
}
});
System.out.println(a); //[Tom, king, milan]
Collections.swap(a,0,2);
System.out.println(a); //[milan, king, Tom]
}
注意一下copy方法参数位置以及内存不足会报错即可。
public static void main(String[] args) {
List a = new LinkedList();
a.add("Tom");
a.add("king");
a.add("milan");
Object max = Collections.max(a); //返回值是一个Object
System.out.println(max); //milan 字符串最大
Object max2 = Collections.max(a, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length()-((String)o2).length();
}
});
System.out.println(max2);//milan,长度最大
System.out.println(Collections.frequency(a,"Tom"));//1
List b = new LinkedList();
// Collections.copy(b,a); 报错 这时b还没有内存空间
//后面是被复制的,根据源码,如果 a.size>b.size 会抛出异常
for(int i = 0;i<5;i++){
b.add("");
} //扩大b的内存
Collections.copy(b,a);
System.out.println(b); //[Tom, king, milan, , ] 剩下俩是""
Collections.replaceAll(a,"Tom","Jack");
System.out.println(a); //[Jack, king, milan]
}
一个关于HashCode的练习题
当把p1的name改成"CC"时,由于hashCode方法已经改写,因此p1的hash值也因此改变。通过remove的源码得知,删除的原理是根据hash码计算的一个值,因此会计算出p1对应的hash值。但问题是,计算出P1在三号位上(反正不是1号位),但是P1实际上还在一号位,因此删除失败!
加入一个 (1001,"CC")的Person时,根据hashCode计算出安放的位置(当然调用了底层代码,不是直接就是hash值),虽然跟P1hash值完全相同,但是3号位上并没有东西,因此成功安放。
加入一个 (1001,"AA")的Person时,计算出安放位置为1号位,此时1号位有P1,因此会调用equals方法跟P1进行比较,因为P1已经修改过name了,因此不同,放在P1后面(链表) 。