41.链表LinkedList
链表的基本单元是节点;
单向链表每一个节点都有两个属性:①下一个节点的内存地址,②存储的数据;
单向链表
链表的优点:因为在内存空间上地址不连续,所以链表的增删不会导致大量元素位移,随机增删元素效率较高;缺点:查询效率较低,每一次查找都需要从头部节点开始遍历;
如果在实际业务中随机增删较多的情况下,建议用LinkedList存储数据;
在实际开发中添加元素一般是往末尾添加,所以ArrayList使用较多;
双向链表
LinkedList没有初始容量,内部变量first与last初始值为null;
实际开发中,不管是LinkedList还是ArrayList,写代码的时候不需要关心具体是哪个集合,因为是面向接口编程,调用的方法都是接口中的方法;
表示底层用了数组
//ArrayList l = new ArrayList<>();
//表示底层用了双向链表
LinkedList l = new LinkedList<>();
l.add("aaa");
l.add("aaa1");
l.add("aaa22");
for (int i = 0; i < l.size(); i++) {
System.out.println(l.get(i));
}
42.Vector
底层是数组,初始容量是10,扩容后是原容量的2倍;
Vector所以方法都带synchronized修饰,是线程同步的,是线程安全的,效率较低,使用较少了;
Vector l = new Vector();
l.add("aaa");
l.add("aaa1");
l.add("aaa22");
Iterator it = l.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
43.泛型初步
使用泛型Movie表示,集合中只能存储Movie类型的数据;
用泛型来指定集合中存储的数据类型,使用泛型之后集合中的数据类型更加统一了;
List<Movie> l = new ArrayList<Movie>();
l.add(new LoveMovie());
l.add(new WarMovie());
// 表示迭代器迭代的是Movie类型
Iterator<Movie> it = l.iterator();
while (it.hasNext()) {
//使用泛型后,不需要强制类型转换了,每次迭代器返回的都是Movie类型
it.next().seeMovie();
}
泛型语法机制,只是在程序编译阶段起作用,只是给编译器参考的,运行阶段没用;
泛型的优点:集合中的元素类型统一,如果迭代器使用泛型,就不需要强制类型转换了;
泛型的缺点:集合中存储的元素缺乏多样性;
大多数业务中,集合的类型是统一的,所以泛型特性被认可;
自定义泛型
public class Movie<E> {
public String name;
public void leave(E e) {
System.out.println(e);
}
}
public static void main(String[] args) {
Movie<String> m = new Movie<>();
m.leave("aaa");
}
java源码中常用E(element)和T(type)自定义泛型;
44.自动类型推断
在JDK8之后推出了自动类型推断机制,又称为钻石表达式;
//JDK8之后,后面的ArrayList的泛型内容可以不写
List<Movie> l = new ArrayList<>();
45.Map
Map和Collection没有继承关系;
Map集合以key-value的形式存储数据;
key和value都是引用数据类型;
key和value都是存储对象的内存地址;
key起主导作用,value是key的附属品;
Map的常用方法:
Map<Integer, String> m = new HashMap<>();
// 向Map集合添加键值对
m.put(1, "无间道");
m.put(2003, "阿凡达");
m.put(33, "八佰");
// 通过key获取value值
String s = m.get(2003);
System.out.println(s);
// 获取键值对的数量
System.out.println(m.size());
// 通过key删除key-value
m.remove(1);
// 判断是否包含某个key
// contains方法底层调用的是equals,所以自定义类型需要重写equals方法
System.out.println(m.containsKey(33));
// 判断是否包含某个value
System.out.println(m.containsValue(new String("八佰")));
// 获取所以value
Collection<String> v = m.values();
for (String str : v) {
System.out.println(str);
}
// 清空集合
m.clear();
// 判断是否为空
System.out.println(m.isEmpty());
遍历Map
Map<Integer, String> m = new HashMap<>();
// 向Map集合添加键值对
m.put(1, "无间道");
m.put(2003, "阿凡达");
m.put(33, "八佰");
Set<Integer> keys = m.keySet();
Iterator<Integer> it = keys.iterator();
//方式一
while (it.hasNext()) {
Integer key = it.next();
System.out.println(m.get(key));
}
//方式二
for (Integer integer : keys) {
System.out.println(m.get(integer));
}
方式三
Set<Map.Entry<Integer, String>> s = m.entrySet();
Iterator<Map.Entry<Integer, String>> it2 = s.iterator();
while (it2.hasNext()) {
Map.Entry<Integer, String> node = it2.next();
Integer key = node.getKey();
String val = node.getValue();
System.out.println(key + "--" + val);
}
//或者
//此方式不必先获取key再通过key获取value,所以效率较高,适合大数据量遍历
for (Map.Entry<Integer, String> entry : s) {
System.out.println(entry.getKey() + "---" + entry.getValue());
}
46.HashMap集合
HashMap集合底层是哈希表/散列表的数据结构;
哈希表是一个数组和单向链表的结合体;
数组:在增删数据的时候效率较低,在查询的时候效率较高;
单向链表:在增删数据的时候效率较高,在查询的时候效率较低;
哈希表将以上两种数据结构融合在一起,充分发挥他们各自的优点;
类似:数组里面是一个个链表;
源码解析
//哈希表是一维数组,数组的每一个元素是一个单向链表,是数组和单向链表的结合体;
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
//hash值是key的hashCode()的执行结果,hash值通过hash函数/算法,可以转换存储成数组的下标
final int hash;
final K key; //存储到map集合的key
V value; //存储到map集合的value
Node<K,V> next; //下一个节点的内存地址
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
hashMap的key部分的特点是无序不可重复的:因为节点不一定挂载到哪一个单向链表上,所以是无序的,equals方法判断两个key如果相同,那么新的value会覆盖旧的value,所以是不可重复的;
由于hashMap集合中的key部分的元素其实就是放到了hashSet集合中了,所以hashSet集合中的元素也需要同时重写hashCode和equals方法;
假设所有的haseCode方法返回一个固定值,那么会导致底层哈希表变成了单向链表,哈希表hashMap使用不当时无法发挥性能;这种情况我们称为散列分布不均匀;
如果有100个元素,分布在10个单向链表,每个单向链表有10个元素,这是最好的,是散列分布均匀的;
如果所有的hashCode方法返回的值都不同,那么就会导致哈希表的底层变成了一维数组,没有链表的概念了,这种情况也是散列分布不均匀的;
散列分布均匀需要重写hashCode方法时有一定的技巧;
hashMap底层数组的容量是16,默认加载因子是0.75,就是当hashMap底层数组的容量达到75%的时候,数组开始扩容,扩容之后的容量是原容量的2倍;
hashMap的初始化容量必须是2的n次方(2,4,8,16...),这是官方推荐的,这是为了达到散列均匀,提高hashMap集合的存储效率,所必须的;
向map集合中存和取都是先调用hashCode方法再调用equals方法,equals有可能调用,有可能不调用;
put(k,v)中,k的hashCode方法返回哈希值,哈希值通过哈希算法转成数组下标,如果数组下标不存在,则直接添加节点,equals方法不执行;
get(k)中,k的hashCode方法返回哈希值,哈希值通过哈希算法转成数组下标,如果数组下标没有元素,则返回null,equals方法不执行;
如果一个类的equals方法重写了,那么它的hashCode方法必须重写,并且如果equals方法返回的是true,那么两个对象hashCode方法返回值必须一样;
equals方法返回true,表示两个对象相同,在同一个单向链表上比较,那么对于同一个单向链表上的节点来说,他们的哈希值是相同的,所以hashCode方法返回的值也应该相同;
终极结论:放在hashMap集合的key部分的元素与hashSet集合的元素,他们的hashCode方法和equals方法必须重写;
JDK8之后,hashMap里面的单向链表中的元素如果超过8个,单向链表这种数据结构会变成红黑树数据结构,当红黑树的元素少于6个,则红黑树会变成单向链表数据结构;
//参考源码
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
这种方式也是为了提高检索效率,二叉树的检索会再次缩小检索范围,提高效率;
对于哈希表数据结构来说:
如果o1和o2的哈希值相同,一定是同一个单向链表上,当然,如果o1和o2的哈希值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生哈希碰撞
47.hashTable
hashTable的key和value都不能是null,否则报空指针异常;
hashTable方法都带有synchronized修饰符,是线程安全的;
现在线程安全有其他的解决方案,这个hashTable对线程安全的处理,导致效率低下,使用较少了;
hashTable与hashMap一样,底层都是哈希表数据结构;
hashTable的初始化容量是11,默认加载因子是0.75;
hashTable的扩容是原容量*2+1;
48.Properties
properties是一个Map集合,继承Hashtable,Properties的key和value都是string类型;
properties被称为属性类对象;
properties是线程安全的;
49.TreeSet
TreeSet底层是TreeMap;
TreeMap集合底层是二叉树;
TreeSet中的元素,等同于放到TreeMap中的key部分;
TreeSet集合中的元素无序不可重复,但是可以按照元素的大小自动排序,称为可排序集合;
TreeSet<String> ts = new TreeSet<>();
ts.add("ywxk");
ts.add("aaa");
ts.add("kkk");
ts.add("ccc");
// 按照字典升序
for (String str : ts) {
System.out.print(str);
}
TreeSet<Integer> ts2 = new TreeSet<>();
ts2.add(300);
ts2.add(100);
ts2.add(500);
ts2.add(200);
for (Integer integer : ts2) {
System.out.print(integer);
}
TreeSet无法对自定义类型排序,因为没有指定自定义类型之间的比较规则,导致自定义类型在转换成Compare类型时异常,所以异常名称是java.lang.ClassCastException;
放在TreeSet集合中的元素需要实现java.lang.Comparable接口,并且实现CompareTo方法;
public class Movie implements Comparable<Movie> {
double price;
String mName;
public Movie(double p, String name) {
this.price = p;
this.mName = name;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "price=" + price + "--name=" + mName;
}
@Override
public int compareTo(Movie o) {
// TODO Auto-generated method stub
//写比较规则
int p = (int) (this.price - o.price);
if (p == 0) {
return this.mName.compareTo(o.mName);
} else {
return p;
}
}
}
compareTo方法底层是二叉树查找;
除了重写compareTo方法比较TreeSet元素之外,还可以用比较器方式;
比较器实现java.util.comparator接口(Comparable是java.lang包下的,比较器是java.util包下的);
//new HashSet传入比较器方式
public static void main(String[] args) throws Exception {
test1();
}
public static void test1() {
WarMovie wm1 = new WarMovie(1999);
WarMovie wm2 = new WarMovie(2009);
WarMovie wm3 = new WarMovie(2003);
WarMovie wm4 = new WarMovie(1988);
TreeSet<WarMovie> ts = new TreeSet<>(new Compare());
ts.add(wm1);
ts.add(wm2);
ts.add(wm3);
ts.add(wm4);
for (WarMovie warMovie : ts) {
System.out.println(warMovie);
}
}
class Compare implements Comparator<WarMovie> {
@Override
public int compare(WarMovie o1, WarMovie o2) {
// TODO Auto-generated method stub
return o1.year - o2.year;
}
}
//匿名内部类方式
//类没有名字,new接口
TreeSet<WarMovie> ts = new TreeSet<>(new Comparator<WarMovie>() {
@Override
public int compare(WarMovie o1, WarMovie o2) {
// TODO Auto-generated method stub
return o1.year - o2.year;
}
});
放到TreeSet或者TreeMap的key部分的元素如果想要做到排序,可以有两种方式;
①放入集合的元素实现Comparable接口的compareTo方法;
②在new TreeSet的时候传入一个实现Comparator接口的compare方法的比较器;
当比较规则不会发生改变或者比较规则只有一个的时候,选择实现Comparable接口;
当比较规则有多个,或者经常需要切换的时候,选择实现Comparator接口;
Comparator的设计比较符合OCP原则,一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展;
50.自平衡二叉树
自平衡二叉树遵循左小右大原则存放,所以存放的时候需要比较;
遍历二叉树的方式有3种:
前序遍历:根左右;
中序遍历:左根右;
后序遍历:左右根;
前中后指的是跟的位置;
TreeSet/TreeMap集合,Iterator迭代器采用的是中序遍历,即左根右;
存入按照大小存放,取出来的时候就自然按照大小排序了;
//源码
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
在TreeSet存入元素的时候,会查找合适的树位置,找到后将元素存入这个位置;