第十六章 数组
1. 数组为什么特殊
- 数组的效率高(唯一优点),它是一个简单的线性序列。
- 数组之所以优于泛型之前的容器,就是因为可以创建一个树去持有某种基本类型,意味着可以通过编译期检查,防止插入错误类型和抽取不当类型。
2. 数组是第一级对象
- 对象数组保存的是引用,基本类型数组保存的是值。
- 数组的初始化:
- 基本类型:数值型初始化为0,char型初始化为 (char)O,boolean型初始化为false。
- 对象类型:初始化为null。
- 数组也是一个对象,和其他普通对象一样在堆中创建, int[ ] arr arr是数组的引用。
- 可以隐式创建数组对象,也可以new显式创建数组对象
int[] ints = {1 ,8 ,9}; //聚集初始化
/*动态聚集初始化,任意位置创建并初始化,
* 有时候传一个数组类型参数时代码更简单*/
int[] iArr = new int[]{2 , 5 , -12 , 20};
int[] arr = new int[3];//只定义了大小
- 对象数组中数组存的是对象的引用,基本类型数组直接存值
- length表示数组大小,不表示数组内具体存有多少个元素。
3. 返回一个数组
- 和返回一个普通类型没区别
4. 多维数组
- Java没有多维数组,任何多维数组都可以看成一维数组内引用一维数组
- 初始化多维数组时可以先只初始化最左边的维数,此时该数组的每个元素都相当于一个数组引用变量,这些数组元素还需要进一步初始化
- int a = new int[2][3][5] ; 直接定义大小,这样的数组是个规则的多维数组
- 逐步定义大小如下:
int[][][] a = new int[2][][];
System.out.println("a.length="+a.length); //a中只有2个元素a[0],a[1]它们是一个二维数组的引用
a[0]=new int[3][];
a[1]=new int[3][];
System.out.println("a[1].length="+a[1].length);//a[1]中3个元素a[1][0],a[1][1],a[1][2]他们是一维数组的引用
a[0][1] = new int[5];
System.out.println("a[0][1].length="+a[0][1].length);// a[0][1] 中有5个元素a[0][1][0]-a[0][1][4]
System.out.println(Arrays.deepToString(a));
/* a.length=2
a[1].length=3
a[0][1].length=5
[[null, [0, 0, 0, 0, 0], null], [null, null, null]]
*/
- 逐步定义大小可以定义出不规则多维数组,如
a[0]=new int[3][];
a[1]=new int[2][];
- 打印多维数组Arrays.deepToString();
- 数据存在[5]这个数组中,其他[2]和[3]都存的引用。
5. 数组与泛型
- 可以创建泛型数组引用 Fruit[ ] apples;
- 但不能实例化具有泛型的数组如 apples = new Fruit[ ];
- 数组和泛型不能很好的结合,因为数组必须知道它所持有的确切类型,以强制保证类型安全。
- 应该首选参数化方法,次选参数化类。更加灵活。
6. 填充数组
- Array.fill(Object[] a, int fromIndex, int toIndex, Object val) 使用val值填充范围内的每一个值,没有范围就填充全部值,此方法被重载了很多次可以适用任何类型。缺点就是只能用一个值填充。
7. Arrays类库
- java.util.Arrays 类库是用来操作数组的,全部为static方法
- 复制数组
- 可以使用System.arraycopy(Object src, int srcPos,Object dest, int destPos,int length); 参数有(源数组,偏移量,目标数组,偏移量,长度)长度+源/目标数组偏移量后不能越界。会将目标数组偏移量后指定长度的元素替换为源数组的偏移量后指定长度的元素,此方法是本地方法直接内存操作具有较高的效率,但对于对象类型数组只是复制了一份引用而已,并没有复制对象,这也叫浅度复制。并且没有实现自动包装。
- 1.6开始 可以使用 Arrays.copyO f(int [] original, int newLength) ,复制一份数组如果新数组长度大于源数组长度则用0或null填充,属于深复制。
- copyOfRange(char[] original, int from, int to) 深度复制
- 数组比较
- Arrays.equals(a1, b1) 相同的条件是数组元素个数相等,相同位置元素内容相同。Arrays的equals()是重载过的是基于内容比较。
- 多维数组使用Arrays.deepEquals()比较。
- 数组元素比较
- 一个类实现Comparable接口,重写compareTo()方法后就具有了比较能力。,至于什么跟什么比可以根据要求决定写在compareTo(Object a)方法里,
- 如果指定的数与参数相等返回0。
- 如果指定的数小于参数返回 -1。
- 如果指定的数大于参数返回 1
- 实现Comparable接口后调用Arrays.sort()方法就会自动升序排序
- 一个类实现Comparable接口,重写compareTo()方法后就具有了比较能力。,至于什么跟什么比可以根据要求决定写在compareTo(Object a)方法里,
- 数组排序
- Arrays.sort(Object[] a) a 必须实现Comparable接口
- Arrays.sort(T[] a, Comparator<? super T> c) Comparator比较器接口,可以创建自己需要的比较规则在compare(Object a, Object b)方法实现即可。可以对没有实现Comparable接口的类或者Comparable比较方式不符合要求的对象按自己需求比较.
- o1大于o2,返回正整数
- o1等于o2,返回0
- o1小于o3,返回负整数
- Comparable.compareTo(Object a)也称自然排序,内比较器,自己内的元素排序。Comparator.compare(Object a, Object b) 外比较器,比较对象属性,无法对基本类型数组排序。
- 在已经排序的数组中查找
- Arrays.binarySearch(Object[] a, Object key) 如果找到返回索引,找不到返回一个负值,该负值= -(插入点)-1 ,插入点为第一个比Key大的元素的索引。
- 对于使用了Comparator排序的对象数组要使用 binarySearch(T[] a, T key, Comparator<? super T> c) 查找
- 基本数据类型数组无法使用 binarySearch(T[] a, T key, Comparator<? super T> c)
第十七章 容器深入研究
1. 容器分类
- 容器分为Collection集合类,和Map键值对类2种
- 使用最多的就是第三层的容器类,其实在第三层之上还有一层Abstract 抽象类,如果要实现自己的集合类,可以继承Abstract类,而不必实现接口中的所有方法。
- Collection 接口
- List 接口 (按插入顺序保存,元素可以重复)
- ArrayList (相当于大小可变的数组,随机访问快,插入移除慢)
- LinkedList(插入移除快,随机访问慢,也实现了Queue接口)
- set 接口(不能有重复元素)
- HashSet(元素无序,查询速度非常快)
- LinkedHashSet(按插入顺序保存,同时有HashSet的查询速度)
- TreeSet(按元素升序保存对象,或者根据元素实现的比较器排序)
- HashSet(元素无序,查询速度非常快)
- Queue接口(一头进一头出)
- PriorityQueu(优先级高的先出,也实现了List接口)
- List 接口 (按插入顺序保存,元素可以重复)
- Map接口
- HashMap (查找速度快,内部无规则排序)
- LinkedHashMap(按插入顺序排序,查找速度快)
- TreeMap(按升序保存对象,或者根据元素实现的比较器排序)
- HashMap (查找速度快,内部无规则排序)
2. 填充容器
- 所有Collection的构造器都可以接收另一个Collection(可以不同类型)来填充自己。
- 数组有Arrays类填充,容器也有Collections类填充,这种工具类中一般都是静态方法不用创建它们的对象直接调用,所以很方便
- fill(list, T obj)方法都只是复制一份对象的引用,并没有额外创建对象,并且只能填充List,它会将容器内的元素清空再添加元素。
- nCopies(int n, T o) 返回一个List 功能和fill一模一样。
- addAll( list, T … obj) 将元素添加到集合,集合本身也有addAll()方法并且还可以指定位置开始添加
3. Collection
- Collection中的方法在List和Set中都实现了,List还添加了额外的方法,如get(),这在Collection和Set中都没有,因为Set无序所以无法确定位置。
4. collection中的可选操作
- 可选的方法就是该方法在父类中会抛出异常,如果子类不需要该方法就不必重写它,一但调用则抛出异常,如果需要就去重写它的功能。
- Collection中的 各种添加 移除方法都是可选的。AbstractList ,AbstractSet,AbstractQueue中就是实现了可选功能,调用这些抽象类中的方法就会抛出异常。
5. List
- jdk1.8 中ArrayList的Add方法实现原理: 如果elementData中元素数大于10个则复制一份旧数组并扩容再将元素添加就去
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//关键代码:elementData = Arrays.copyOf(elementData, newCapacity);
elementData[size++] = e;
return true;
}
- remove方法 使用本地方法直接操作内存改变,但这也属于浅复制多线程下可能复制出错导致删不掉元素, numMoved = size - index - 1; 最后将数组最后一个元素设为null
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
- 值得注意的是 size是ArrayList内元素个数,并不是数组长度,ArrayList内数组长度默认最小是10
- LinkedList 内部是由一个内部静态类实现的一个双向链表
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
6. Set
- HashSet底层是由HashMap实现的,add进去的值就put在了map中作为key值,为了减小开销,所有value值为同一个new Object() 。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- 保证不重复:先检测有没有相同的hash值,如果没有就添加元素,如果有在equal比较key值,相同则不允许添加,不同则允许添加入map
- LinkedHashSet也是由 LinkedHashMap实现
- TreeSet底层由TreeMap实现。TreeSet 实现了NavigableSet接口,而NavigableSet接口继承了SortedSet接口 ,NavigableSet提供了搜索功能方法,SortedSet 提供了排序功能方法,
- TreeSet(Comparator<? super E> comparator) ,凡是带有比较排序功能容器都有一个能传入比较器Comparator对象的构造方法,使用这个构造器可以根据自己实现的Comparator排序。
7. Queue
- 除了并发应用外,Queue的实现只有LinkedList 和 PriorityQueue,这两个Queue的差异在于排序行为,而不是性能。队列行为在于add是添加到队头,remove移除队尾元素。
- PriorityQueue(Comparator<? super E> comparator) 可以使用自己的比较器比较,根据对象内某个属性设置在队列中的优先级
8. Map
- HashMap的底层数据结构:
- 散列表(哈希表)即 数组+单链表 默认数组大小是16,数组大小一直保持是2n, 数组下标是 key的hash值进行扰乱 再与 数组大小减1 按位 做 & 运算得出。
- 扰乱的目的:为了使得到的值更加均匀。
- 减1的目的: 2n - 1 之后低位都是1,进行与运算后只要有一位不同那么就能的到不同的结果,减小了哈希冲突。同时结果范围也正好在数组大小相同。
- 链表的结构
- jdk1.8 Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
- 单链表也称桶(bucket)
- 单链表是为了解决哈希冲突,不同的key可能计算出相同的hash值,如果哈希值冲突了那么就用头插法将新值插入链表的头,作为数组中的元素,不用尾插法能省去了遍历链表的开销。
- 如果key=null 那么key会一直在数组中作为链表的头。
- 负载因子
- 默认0.75 当map的桶位数(键值对数量)达到 数组容量*0.75 时就会进行扩容 ,扩容后的数组是原来的2倍。
- jdk1.8 扩容时的操作:
- 对旧链表正向遍历,检查hash值新增的高位(原来和1111做&运算,扩容后与11111做&运算,多了一位要比较的)如果是0那么还在原来数组位置,如果为1则在当前位置+扩容量 后的数组位置。
- 1.8 扩容后新增元素是用尾插法,所以不会出现循环,倒置链表,所以jdk1.8下put不会出现循环链表。
- jdk1.8 对HashMap作出的改变
- 底层数据结构变为 数组+链表+红黑树。解决了链表可能过长的问题,时间复杂度变为O(log n)
- jdk1.8 链表转化红黑树
- 限定了一些阀值
- 桶的树化阀值: TREEIFY_THRESHOLD = 8 当链表长度大于8时将链表转为红黑树。
- 桶的链表还原阈值:UNTREEIFY_THRESHOLD = 6 当扩容后重新计算位置后若红黑树内结点少于6个则红黑树转化为链表
- 最小树化阀值 MIN_TREEIFY_CAPACITY = 64 只有当桶位数大于改64时才进行树化,并且以后树化不能小于 4*TREEIFY_THRESHOLD
- 查看红黑树
- 左子树右子树的判定
- 先通过comparableClassFor 反射获取key对象的所有接口查看有没有实现comparable接口
- 如果key值实现了comparable接口并且compareTo能正确执行并且key值和父结点是同一类型那么执行compareTo方法比较大小,如果大于父节点那么作为右孩子,如果小于父节点作为左孩子。
- 如果比较是相等的,或者没有实现接口则进入决胜局方法tieBreakOrder(),先比较他们的类名字符串如果是同一类型则类名比较就比较不出来,再调用本地方法identityHashCode()生成hash值,比较哈希值,如果哈希值相等返回-1,说明这两个对象在同一个对象,在同一块内存位置。
- 关于jdk1.8 的性能问题就在这里,如果Key 值没有实现comparable接口或者comparaTo方法不能的到正确结果,那么实现红黑树的性能没有只使用链表的高。
- 限定了一些阀值
- get()取值:
- 计算出hash(key) 再通过key.equals()比较取出值
- 那些类做key值比较好?
- String ,Integer 包装器类型,因为他们是final 型 key不会改变,不会出现放进map之后key被改变的情况,并且重写了hashCode()和equals()方法不会出现计算错误。
- Object 对象做key值时要注意到上面的点。
- hashCode()和 equals()方法重写
- 重写equals方法也要重写hashCode方法这是因为 Object 中equals方法比较的是对象的地址,hashCode方法是根据对象地址生成的(字符串对象除外,相同的字符串有相同的hash值,哈希值是根据内容生成的。但字符串==仍然比较的是地址而不是哈希值)。
- 如果重写equals方法时是使用对象中某个条件判断他们相等,那么你再创建一个你认为相等的对象,但他两地址不一样,所以在没有重写hashCode方法后,他们的hashCode就不一样,这样存入map后使用后者取值就无法的到正确结果。
- 重写hashCode方法要保证 如果 equals 相同 那么hashCode一定相同,hashcode相同equals不一定相同,但这样会有哈希冲突,所以一个产生一个尽可能散列的hashCode方法非常重要。
- 内容相同的 字符串 内存中不止有一个,== 比较他们时 false, equals比较时为true ,哈希值相等。
9. Set和存储顺序
- 加入Set的元素必须定义equals()方法以确保对象的唯一性。
- hashCode()只有这个类被置于HashSet或者LinkedHashSet中时才是必需的。但是对于良好的编程风格而言,你应该在覆盖equals()方法时,总是同时覆盖hashCode()方法。
- 如果一个对象被用于任何种类的排序容器中,例如SortedSet(TreeSet是其唯一实现),那么它必须实现Comparable接口。
- 注意,SortedSet的意思是“按对象的比较函数对元素排序”,而不是指“元素插入的次序”。插入顺序用LinkedHashSet来保存。
10. Stack和Vector
- Vector 类可以实现可增长的对象数组。(和ArrayList相似)与数组一样,它包含可以使用整数索引进行访问的组件。但是,Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。
- Stack是继承于Vector(矢量),由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。
- Vector与ArrayList的最大区别就是Vector是线程安全的,而ArrayList不是线程安全的。另外区别还有:
- ArrayList不可以设置扩展的容量,默认1.5倍;Vector可以设置扩展的容量,如果没有设置,默认2倍
- ArrayList的无参构造方法中初始容量为0,而Vector的无参构造方法中初始容量为10。
- Vector线程安全,ArrayList线程不安全。
- Vector和它的子类Stack都是线程安全的集合。
11. 接口的不同实现
- ArrayList底层是数组所以随机访问非常快,但添加删除时要复制数组,添加删除的代价较大。
- LinkedList 内部是双向链表所以插入删除代价低,对于随机访问会有顺着结点一个一个查找过程,所以速度较慢,但如果在2个端点插入或删除,会做特殊处理速度较快。
- TreeSet存在的唯一原因就是他可以维持元素的排序状态。
12. 实用方法
- List的排序与查询所使用的方法与对象数组使用的方法有相同的名字和语法,Collections的static方法相当于代替了Arrays的静态方法而已。
- Collection 和 Map 可以设置为只读 。Collections的UnmodifiableXXX( )方法设置不同collection和Map为只读。
- Java容器类库的容错机制: 但遍历一个容器或者拿到了该容器的迭代器后改变容器状态如添加一个元素删除一个元素修改某个元素则会抛出异常。
13. 持有引用
- Java.lang.ref类库包含了一组类,这些类为垃圾回收提供了灵活性。
- 三个继承Reference抽象类的类:SoftReference, WeakReference, PhantomReference,如果某个对象只能通过这三个对象才可以获得,那么GC会对这个对象作出不同的回收。
- 这三个容器类用来保存对象的引用。
- 对象可获得:栈中有一个普通引用可以直接指向这个对象,或者通过不同的对象间接指向一个对象,那么这个对象就是可获得的或者可达的,可获得的对象是不能被回收的。
- 普通引用:也称强引用,没有被Reference包装的引用,通过普通引用可获得的对象不能被释放。
- 如果一个对象被普通引用指向,那么他就不能被释放,一直占据内存,如果没有引用指向那么就会被回收,如果有个对象希望以后还能访问到但是也希望内存不足时可以回收那么对这类对象的引用就可以放在Reference里
- Reference对象的可获得性由强到弱,越强越不容易被回收:
- SoftReference 软引用 ,用来实现内存敏感的高速缓存,如果内存即将溢出时就回收对象。
- WeakReference 弱引用 用来“规范映射”而设计的,WeekHashMap中的key就是WeekReference。
- PhantomReference 虚引用 如果有个对象只有虚引用了那么他就会被回收。
- ReferenceQueue :GC时会在这种队列(可以自己创建,jvm也会自动创建)中查找虚引用,然后把虚引用的对象清理,softreference 和 weekreference 可以放也可以不放如ReferenceQueue中,但PhantomReference必须在ReferenceQueue中。
- WeakHashMap :key 保存弱引用,value 保存其他对象,当key弱引用指向的对象没有其他强引用引用那么key-value就会被回收。如果是普通HashMap那么key指向的对象除了多了一个HashMap的引用,还需要手动清理HashMap。
第十九章 枚举
1. 初识枚举
- 枚举实例 必须被定义在方法的前面
- 最后一个枚举实例后面必须添加一个";"
- 构造器的访问权限必须是 private 或者 package
public enum MediaOwnerTypeEnum {
/**养老院 */
BEADHOUSE("BeadHouse"),
/** 学校 */
SCHOOL("School"),
/** 教师 */
TEACHER("Teacher"),
/** 用户 */
USER("User"),
/** 身份证 */
IDCARD("IDCard"),
/**首页banner静态页**/
HOMEAREAPAGE("HomeAreaPage");
private String owner = "";
//构造器
MediaOwnerTypeEnum(String owner) {
this.owner = owner;
}
@Override
public String toString() {
return owner;
}
}
举例2:
public enum AppointmentStatusEnum {
//枚举实例 必须被定义在方法的前面
APPOINTMENT(1, "已预约"),
NUMBERED(2, "已取号"),
CANCELLED(3, "已取消"),
//最后一个枚举实例后面必须添加一个";"
EXPIRED(4, "已失约");
private Integer status;
private String statusName;
//构造器的访问权限必须是 private 或者 package
AppointmentStatusEnum(Integer status, String statusName) {
this.status = status;//枚举的第一个参数
this.statusName = statusName;//枚举的第二个参数
}
//get set方法
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getStatusName() {
return statusName;
}
public void setStatusName(String statusName) {
this.statusName = statusName;
}
}
2. values()的神秘之处
- 通过反编译枚举类,values()是由编译器添加的static()方法。编译器将枚举类(enum)标记为final类,所以enum类无法被继承。
3. 实现而非继承
- 所有的enum类都继承自java.lang.Enum类。由于Java不支持多重继承,所以你的enum类不能再继其他类,然而,在我们创建一个新的enum时,可以同时实现一个或多个接口。
4. 使用EnumMap
- 与EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序。
- 常量相关的方法(constant-specific methods)。
- 多路分发(multiple dispatching)。
5. 常量相关的方法
- 通过相应的enum实例,我们可以调用其上的方法。这通常也称为表驱动的代码(table-driven code,请注意它与前面提到的命令模式的相似之处)。
- 使用enum的职责链
- 职责链(Chain of Responsibility)
- 使用enum分发
- 一种方式是使用构造器来初始化每个enum实例,并以“一组”结果作为参数。这二者放在一块,形成了类似查询表的结构。
[link] https://blog.csdn.net/wszcy199503/article/details/80404816
- 一种方式是使用构造器来初始化每个enum实例,并以“一组”结果作为参数。这二者放在一块,形成了类似查询表的结构。
6. 多路分发
- 多种类型交互时有时并不能确定所有类型,如: NUM.complete(NUM) , NUM 是所有数字类型的超类,a.complete(b) ,a b可能是同种类型也可能不是同一种类型。
- Java 动态绑定只能处理一种类型,属于单路分发(分派),动态绑定能将complete绑定到分路a。只有方法调用才会执行动态绑定。
- 可以为每一个分发实现自己的动态绑定
public enum Outcome { WIN, LOSE, DRAW } ///:~
interface Item {
Outcome compete(Item it);
Outcome eval(Paper p);
Outcome eval(Scissors s);
Outcome eval(Rock r);
}
class Paper implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return DRAW;
}
public Outcome eval(Scissors s) {
return WIN;
}
public Outcome eval(Rock r) {
return LOSE;
}
public String toString() {
return "Paper";
}
}
class Scissors implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return LOSE;
}
public Outcome eval(Scissors s) {
return DRAW;
}
public Outcome eval(Rock r) {
return WIN;
}
public String toString() {
return "Scissors";
}
}
class Rock implements Item {
public Outcome compete(Item it) {
return it.eval(this);
}
public Outcome eval(Paper p) {
return WIN;
}
public Outcome eval(Scissors s) {
return LOSE;
}
public Outcome eval(Rock r) {
return DRAW;
}
public String toString() {
return "Rock";
}
}
public class RoShamBo1 {
static final int SIZE = 20;
private static Random rand = new Random(47);
public static Item newItem() {
switch (rand.nextInt(3)) {
default:
case 0:
return new Scissors();
case 1:
return new Paper();
case 2:
return new Rock();
}
}
public static void match(Item a, Item b) {
System.out.println(a + " vs. " + b + ": " + a.compete(b));
}
public static void main(String[] args) {
for (int i = 0; i < SIZE; i++)
match(newItem(), newItem());
}
}
- 使用enum实现多路分发
- enum的实例不能作为类型参数,不可以重载方法。
- 可以使用enum构造器初始化每个enum实例,并以一组结果作为参数如 ENUM_A( vsA_DRAW, vsB_LOSE, vsC_WIN ) 在比较方法中使用switch 判断 返回 结果PAPER.complete()时把PAPER构造器中的结果与 OutCome 变量绑定,根据对比的参数返回对比结果,因此实例构造器中的参数位置非常重要
- 使用EnumMap
- EnumMap实现真正的多路分发
- 使用二维数组
- 简单,速度快,代码易懂,但是组数比较大时尺寸容易错