课程链接:https://edu.aliyun.com/course/1012
第1~6章 关于线程:https://blog.csdn.net/weixin_43494837/article/details/113764127?spm=1001.2014.3001.5501
第7~14章 类库+正则+国际化+比较器:https://blog.csdn.net/weixin_43494837/article/details/113764156?spm=1001.2014.3001.5501
第15~20章 关于IO:https://blog.csdn.net/weixin_43494837/article/details/113764173?spm=1001.2014.3001.5501
第21~27章 关于反射:https://blog.csdn.net/weixin_43494837/article/details/112797304?spm=1001.2014.3001.5501
(思维导图在最后)
————————————————————————————————————
第28章:类集框架简介
课时123:类集框架简介
-
类集:一套动态对象数组的实现方案。
-
在实际开发中,没有任何一项开发可以离开数组,但是传统的数组实现起来非常繁琐,而且长度是其致命伤。正因为长度问题,所以传统的数组是不可能大范围使用的,但开发又离不开数组,所以最初就只能依靠一些数据结构来实现动态的数组处理,其中最为重要的两个结构:链表、树。然而这些数据结构的实现,也有如下的一些问题:
- 数据结构的代码实现困难,对于一般的开发者是无法进行使用的;
- 对于链表或二叉树,
- 进行更新处理时,维护是非常麻烦的;
- 还需要尽可能保证操作其的性能。
因此,在JDK1.2,Java引入了类集开发框架。主要就是对常见的数据结构进行完整的实现包装,并提供一系列的接口与实现子类,来帮助用户减少使用数据结构而带来的开发困难。(最初的类集实现,由于Java本身的技术所限,所以对于数据的控制并不严格,全部采用了Object类型进行数据接收;在JDK1.5后,由于泛型技术的推广,所以类集本身也得到了良好的改进,可以直接使用泛型来保存相同类型的数据;并且随着数据量的不断增加,从JDK1.8开始,类集的实现算法也得到了良好的性能提升。)
-
类集框架的核心接口:Collection、List、Set、Map、Iterator、Enumeration、Queue、ListIterator。
课时124:Collection接口简介
-
java.util.Collection是单值集合操作的最大的父接口,在该接口中定义有所有的单值数据的处理操作。(单值:每次操作只能保存一个对象,就类似每次子弹上膛只能上一颗)。
-
Collection包是从JDK1.2开始有的(JDK1.2最大的特征,除了引入图形界面Swing开发包,然后就是类集)。
-
Collection接口的核心方法:
No 方法 类型 描述 1 public boolean add(E e) 普通 保存数据 2 public boolean addAll(Collection<? extends E> c) 普通 追加一组数据(如一个链表连接另一个链表) 3 public void clear() 普通 清空集合(让根节点为空,并进行GC处理) 4 public boolean contains(Object o) 普通 查询数据是否存在(需要equals()方法支持) 5 public boolean remove(Object o) 普通 删除数据(需要equals()方法支持) 6 public int size() 普通 获取数据长度(最大值为Integer.MAX_VALUE) 7 public Object[] toArray() 普通 将集合转为对象数组 8 public Iterator iterator() 普通 将集合转为Iterator接口 以上方法中,【增加】add() 和 【输出】iterator() 这两个最常用。
-
在JDK1.5版本之前,Collection只是一个独立的接口;在JDK1.5后,提供了Iterable父接口,并且在JDK1.8后针对于Iterable接口也得到了一些扩充。另外,在JDK1.2~JDK1.4的时代里,如果要进行集合的使用往往会直接操作Collection接口,而从JDK1.5时代开始,更多情况下选择的都是Collection的两个子接口:允许重复的List子接口、不允许重复的Set子接口。
第29章:List集合
课时125:List接口简介
-
List是Collection的子接口,其最大的特点是允许保存有重复元素数据,该接口的定义如下:
public interface List<E> extends Collection<E>
-
List子接口对于Collection接口进行了方法扩充:
No 方法 类型 描述 1 public E get(int index) 普通 获取指定索引的数据 2 public E set(int index, E element) 普通 修改指定索引的数据 3 public ListIterator listIterator() 普通 将List转为ListIterator -
List接口的常用子类:
- ArrayList
- LinkedList
- Vector
-
从JDK1.9开始,List接口中追加有一些static方法,以方便用户的处理(如of()方法):
import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = List.of("Hello", "World", "Hello", "Pikaqiu"); // System.out.println(list); // 输出:[Hello, World, Hello, Pikaqiu] Object[] objArr = list.toArray(); // 调用Collection接口中的toArray()方法 for (Object obj : objArr) { System.out.print(obj + " "); // 输出:Hello World Hello Pikaqiu } } }
-
在JDK1.8之后,Iterable父接口之中定义有一个forEach()循环输出的方法:
default void forEach(Consumer<? super T> action)
使用forEach()方法实现输出:
import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = List.of("Hello", "World", "Hello", "Pikaqiu"); list.forEach(System.out::println); // 输出: // Hello // World // Hello // Pikaqiu } }
课时126:ArrayList子类
-
ArrayList类的定义:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
-
示例:
import java.util.ArrayList; import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Hello"); list.add("World"); list.add("Hello"); list.add("Pikaqiu"); list.add(null); System.out.println(list); // 输出:[Hello, World, Hello, Pikaqiu, null] System.out.println(list.get(0)); // 输出:Hello } }
可以发现,ArrayList中保存的数据是:
- 重复的
- 顺序存储的
-
forEach()输出实现(非标准输出):
list.forEach((str)->{ // 消费型函数式接口 System.out.print(str + " "); // 输出:Hello World Hello Pikaqiu });
需要注意的是,此种输出并不是正常开发情况下要考虑的操作形式。
-
ArrayList源码分析:
-
无参构造:
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量定义:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
-
单参构造(可开辟指定容量的ArrayList):
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
elementData变量定义:
transient Object[] elementData; // transient:不可序列化
EMPTY_ELEMENTDATA常量定义:
private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认开辟空数组
可以发现,ArrayList实际上封装的仍然是一个数组。
-
add()方法:
如追加数据时,长度不足够,则会开辟一个更大的新数组,并将旧数组的内容拷贝到新数组里,然后再进行追加。
- JDK1.9之后:ArrayList默认的构造只会使用默认的空数组,使用时才会开辟数组,默认的开辟长度为10;
- JDK1.9之前:ArrayList默认的构造实际上就会默认开辟大小为10的数组。
当ArrayList中保存的容量不足时,会采用成倍的方式进行增长,如第一次增长10,第二次增长就是20。
所以再使用ArrayList子类时,一定要估算出数据量有多少,如果超过了10个,那么应该采用有参构造的方法进行创建,以避免产生垃圾数组。
-
课时127:ArrayList保存自定义类对象
-
实现:
import java.util.ArrayList; import java.util.List; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } } public class JavaDemo{ public static void main(String[] args) { List<Person> list = new ArrayList<>(); Person pikaqiu = new Person("皮卡丘", 6); Person xiaozhi = new Person("小智", 12); Person xiaojin = new Person("小进", 12); list.add(pikaqiu); list.add(xiaozhi); list.add(xiaojin); list.forEach(System.out::println); System.out.println("————————————"); list.remove(xiaojin); // remove()方法,不常用 list.forEach(System.out::println); System.out.println("————————————"); System.out.println(list.contains(pikaqiu)); // contains()方法,不常用 } }
课时128:LinkedList子类
-
LinkedList类的定义:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable
-
示例:
import java.util.LinkedList; import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = new LinkedList<>(); list.add("Hello"); list.add("world"); list.add("Hello"); list.add("pikaqiu"); list.add(null); System.out.println(list); // 输出:[Hello, world, Hello, pikaqiu, null] // get()方法:根据索引获取数据内容 System.out.println(list.get(0)); // 输出:Hello } }
可以发现,LinkedList和ArrayList使用是完全一样的,但它们的内部实现机制是完全不同的。
- LinkedList只提供了无参构造,并未提供有参构造。
- 查看源码可以发现,LinkedList封装的其实就是链表的实现。
-
面试题:请问ArrayList与LinkedList有什么区别?
- ArrayList是数组实现的集合操作,而LinkedList是链表实现的集合操作;
- 在使用get()方法时:ArrayList的时间复杂度为“O(1)”,而LinkedList的时间复杂度为“O(n)”(n为集合的长度);
- ArrayList在使用时默认的初始化对象数组的大小长度为10,如果空间不足则会采用2倍形式进行容量的扩充,所以在保存大数据量时,可能会造成垃圾的产生以及性能的下降,而这时候可以使用LinkedList类保存。
课时129:Vector子类
-
Vector是一个原始古老的程序类,这个类是在JDK1.0时提供的。到了JDK1.2时,由于许多开发者已经习惯使用Vector,并且许多系统类也是基于Vector实现的,所以考虑到其使用的广泛性,类集框架将其保留了下来,并让其多实现了一个List接口。
-
Vector类的定义:
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
-
示例:
import java.util.List; import java.util.Vector; public class JavaDemo{ public static void main(String[] args) { List<String> list = new Vector<>(); list.add("Hello"); list.add("world"); list.add("Hello"); list.add("pikaqiu"); list.add(null); System.out.println(list); // 输出:[Hello, world, Hello, pikaqiu, null] // get()方法:根据索引获取数据内容 System.out.println(list.get(0)); // 输出:Hello } }
-
Vector类源码分析:
public Vector() { this(10); } public Vector(int initialCapacity) { this(initialCapacity, 0); } public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; }
可以发现,Vector类的无参构造,是直接开辟一个10长度的数组,而后其余的实现操作与ArrayList是相同的。
并且,Vector类中的操作方法采用的都是synchronized同步处理,而ArrayList并没有进行同步处理,所以Vector类中的方法是线程安全的,但性能不如ArrayList。
第30章:Set集合
课时130:Set接口简介
-
Set集合最大的特点:不允许保存重复元素。
-
在JDK1.9以前,Set集合与Collection集合的定义并无差别;但在JDK1.9后,Set集合也像List集合一样扩充了一些static方法。
-
Set集合的定义:
public interface Set<E> extends Collection<E>
需要注意的是,Set集合并不像List集合那样扩充了许多的新方法,并没有List集合中的get()方法。
-
示例:
import java.util.Set; public class JavaDemo{ public static void main(String[] args) { Set<String> set = Set.of("Hello", "World", "Hello", "Pikaqiu"); // 抛出异常 set.forEach(System.out::println); // 抛出异常: // Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: Hello } }
可以发现,当Set使用of()新方法时,如有重复元素,则会抛出异常,这与传统的Set集合不保存重复元素的特点相一致。
-
Set接口的常用子类:
- HashSet
- TreeSet
课时131:HashSet子类
-
HashSet是Set接口中使用最多的一个子类,其最大的特点就是保存的数据是无序的。
-
HashSet类的定义:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
-
示例:
import java.util.HashSet; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { Set<String> set = new HashSet<>(); set.add("Hello"); set.add("Pikaqiu"); set.add("Hello"); set.add("World"); set.forEach(System.out::println); // 输出: // Hello // World // Pikaqiu } }
可以发现,HashSet中保存的数据是:
- 不重复的
- 无序存储的
课时132:TreeSet子类
-
Set接口的另外一个子接口就是TreeSet,其与HashSet最大区别在于,TreeSet集合里保存的数据是有序的。
-
TreeSet类定义:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable
在这个子类中依然继承了AbstractSet父抽象类,同时又实现了一个NavigableSet父接口:
-
示例:
import java.util.Set; import java.util.TreeSet; public class JavaDemo{ public static void main(String[] args) { Set<String> set = new TreeSet<>(); set.add("Hello"); set.add("Pikaqiu"); set.add("Hello"); set.add("World"); set.forEach(System.out::println); // 输出: // Hello // Pikaqiu // World } }
可以发现,HashSet中保存的数据是:
- 不重复的
- 顺序存储的
课时133:分析TreeSet子类排序操作
-
使用TreeSet保存自定义类:
import java.util.Set; import java.util.TreeSet; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } } public class JavaDemo{ public static void main(String[] args) { Set<Person> set = new TreeSet<>(); Person pikaqiu = new Person("皮卡丘", 6); Person xiaozhi = new Person("小智", 12); Person xiaojin = new Person("小进", 12); // 年龄相同,姓名不同 Person xiaojin2 = new Person("小进", 12); // 数据重复 Person xiaozhi2 = new Person("小智", 3); set.add(pikaqiu); // 抛出异常 set.add(xiaozhi); set.add(xiaojin); set.add(xiaojin2); set.add(xiaozhi2); set.forEach(System.out::println); // 抛出异常: // Exception in thread "main" java.lang.ClassCastException: Person cannot be cast to java.base/java.lang.Comparable } }
从抛出的异常可以发现,TreeSet类允许保存自定义类数据,但自定义类必须要实现Comparable接口。
- TreeSet本质上是利用TreeMap子类实现集合数据的存储,而TreeMap(实质上是树)就需要根据Comparable来确定大小。
给Person类加上实现Comparable接口:
import java.util.Set; import java.util.TreeSet; class Person implements Comparable<Person>{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } @Override public int compareTo(Person person) { if (this.age > person.age) return 1; else if (this.age < person.age) return -1; else return this.name.compareTo(person.name); } } public class JavaDemo{ public static void main(String[] args) { Set<Person> set = new TreeSet<>(); Person pikaqiu = new Person("皮卡丘", 6); Person xiaozhi = new Person("小智", 12); Person xiaojin = new Person("小进", 12); // 年龄相同,姓名不同 Person xiaojin2 = new Person("小进", 12); // 数据重复 Person xiaozhi2 = new Person("小智", 3); set.add(pikaqiu); set.add(xiaozhi); set.add(xiaojin); set.add(xiaojin2); set.add(xiaozhi2); set.forEach(System.out::println); // 输出: // 姓名:小智,年龄:3 // 姓名:皮卡丘,年龄:6 // 姓名:小智,年龄:12 // 姓名:小进,年龄:12 } }
在使用自定义类对象进行比较处理的时候,一定要将该类中所有属性都依次进行大小关系的匹配,否则某一个或者几个属性相同的时候也会被认为是重复数据,所以TreeSet是利用了Comparable接口来确认重复数据的。
由于TreeSet在操作过程之中需要将类中的所有属性进行比对,这样的实现难度太高了(如类有很多个属性),所以在实际的开发中应该首选HashSet子类进行存储。
课时134:分析重复元素消除
-
把上课时的代码中,主类里的TtreeSet换成HashSet:
public class JavaDemo{ public static void main(String[] args) { Set<Person> set = new HashSet<>(); Person pikaqiu = new Person("皮卡丘", 6); Person xiaozhi = new Person("小智", 12); Person xiaojin = new Person("小进", 12); // 年龄相同,姓名不同 Person xiaojin2 = new Person("小进", 12); // 数据重复 Person xiaozhi2 = new Person("小智", 3); set.add(pikaqiu); set.add(xiaozhi); set.add(xiaojin); set.add(xiaojin2); set.add(xiaozhi2); set.forEach(System.out::println); // 输出: // 姓名:小智,年龄:3 // 姓名:小进,年龄:12 // 姓名:小进,年龄:12 // 姓名:小智,年龄:12 // 姓名:皮卡丘,年龄:6 } }
可以发现,输出数据中仍然有重复的。
-
TreeSet类是利用了Comparable接口来实现了重复元素的判断,但HashSet判断重复元素的方式并不是利用Comparable接口完成的,它利用的是Object类中提供的方法实现的:
- 对象编码:public int hashCode()
- 对象比较:public boolean equals(Object obj)
在进行重复元素判断时,首先利用hashCode()进行编码的匹配:
- 如果该编码不存在,则表示数据不存在,证明没有重复;
- 如果该编码存在,则进一步进行对象比较处理,如果发现重复了,则此数据是不允许保存的。
(hashCode()生成的编码就类似身份证号码一样,是根据一定的算法进行生成的。)
-
去除重复元素(重写equals()和hashCode()方法):
( idea编辑器自动equals()和hashCode()方法:Code → Generate → equals() and hashCode() )
class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } }
-
在Java程序中,真正的重复元素的判断处理,利用的就是equals()和hashCode()这两个方法共同作用完成的,而只有在有排序要求的情况下(如TreeSet),才会需要实现Comparable接口。
第31章:集合输出
课时135:Iterator迭代输出
-
集合输出实际上从JDK1.8开始,就在Iterable接口中提供了一个forEach()方法可以进行输出,但它并不是传统意义上的集合输出形式,并且也很难在实际的开发中出现。对于集合操作而言,一共有四种输出形式:Iterator迭代输出(95%)、ListIterator双向迭代输出(0.1%)、Enumeration枚举输出(4.9%)、foreach输出(与Iterator相当)。
-
通过Collection接口的继承关系可以发现,从JDK1.5开始,其多继承了一个Iterable父接口,并且在这个接口里面定义有一个iterator()操作方法,通过此方法可以获取Iterator接口对象(在JDK1.5之前,这一方法直接定义在Collection接口之中):
- 获取Iterator接口对象:public Iterator iterator()
-
Iterator接口的操作方法:
No | 方法 | 类型 | 描述 |
---|---|---|---|
1 | public boolean hasNext() | 普通 | 判断是否还有数据 |
2 | public E next() | 普通 | 获取下一个数据 |
3 | default void remove() | 普通 | 删除数据 |
hasNext()和next()这两个方法,我们在之前使用Scanner类的时候就使用过,而Scanner类就是Iterator接口的子类。
-
示例:
import java.util.Iterator; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { // Set<String> set = Set.of("Hello", "World", "Hello", "Pikaqiu"); // 有重复元素,抛出异常 // 抛出异常:Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: Hello Set<String> set = Set.of("Hello", "World", "Pikaqiu"); Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } // 输出: // World // Hello // Pikaqiu } }
-
Iterator接口的remove()方法:
-
如非必须,请勿使用
-
Collection接口也有remove()方法,但它会导致迭代失败:
import java.util.Iterator; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { Set<String> set = Set.of("Hello", "World", "Pikaqiu"); Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { String str = iterator.next(); if (str.equals("World")) set.remove(str); // 抛出异常 // 抛出异常:Exception in thread "main" java.lang.UnsupportedOperationException else System.out.println(str); } } }
-
使用Iterator接口的remove()方法:
import java.util.HashSet; import java.util.Iterator; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { // 使用Iterator的remove()方法时,set要使用子类进行实例化 Set<String> set = new HashSet<>(); set.add("Hello"); set.add("World"); set.add("Pikaqiu"); Iterator<String> iterator = set.iterator(); while (iterator.hasNext()) { String str = iterator.next(); if (str.equals("World")) iterator.remove(); else System.out.println(str); } System.out.println("————————————"); System.out.println(set); // 输出: // Hello // Pikaqiu // ———————————— // [Hello, Pikaqiu] } }
-
-
面试题:请解释Collection.remove() 与 Iterator.remove() 的区别:
- 在进行迭代输出时,如果使用Colletion.remove(),会造成并发更新的异常,导致程序删除出错;而利用Iterator接口中的remove()方法,则可以在迭代输出中进行正常的删除处理。
课时136:ListIterator双向迭代输出
-
使用Iterator进行的迭代输出操作有一个特点:只允许由前向后输出,而如果现在需要进行双向迭代处理,那么就必须依靠Iterator的子接口:ListIterator接口来实现了。
-
ListIterator类的定义:
public interface ListIterator<E> extends Iterator<E>
需要注意的是,如果想要获取ListIterator接口对象,Collection中并没有定义相关的处理方法,但是List子接口有,也就是说这个输出的接口是专门为List集合准备的。
-
ListIterator接口的操作方法:
No 方法 类型 描述 1 public boolean hasNext() 普通 判断是否还有数据 2 public E next() 普通 获取下一个数据 3 public boolean hasPrevious() 普通 判断是否还有前一个数据 4 public E previous() 普通 获取前一个数据 -
示例:
import java.util.ArrayList; import java.util.List; import java.util.ListIterator; public class JavaDemo{ public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Hello"); list.add("World"); list.add("Pikaqiu"); ListIterator<String> listIterator = list.listIterator(); while (listIterator.hasNext()) { System.out.println(listIterator.next()); } System.out.println("————————————"); while (listIterator.hasPrevious()) { System.out.println(listIterator.previous()); } // 输出: // Hello // World // Pikaqiu // ———————————— // Pikaqiu // World // Hello } }
🔺 如果想实现由后向前的遍历,那么首先要实现的是由前向后实现遍历处理。
(也就是在调用previous()方法之前,需要有调用next()方法,否则previous()方法输出是没有内容的。)
课时137:Enumeration枚举输出
-
Enumeration是JDK1.0时就提供的输出接口,它只为Vector一个类服务。
-
Enumeration类的定义:
public interface Enumeration<E>
-
获取Enumeration对象:
- 通过Vector类的方法:public Enumeration elements()
-
Enumeration接口的操作方法:
No 方法 类型 描述 1 public boolean hasMoreElements() 普通 判断是否还有数据 2 public E nextElement() 普通 获取下一个数据 -
示例:
import java.util.Enumeration; import java.util.Vector; public class JavaDemo{ public static void main(String[] args) { Vector<String> vector = new Vector<>(); vector.add("Hello"); vector.add("World"); vector.add("Pikaqiu"); Enumeration<String> enumeration = vector.elements(); while (enumeration.hasMoreElements()) { System.out.println(enumeration.nextElement()); } // 输出: // Hello // World // Pikaqiu } }
课时138:foreach输出
-
除了使用迭代接口实现输出之外,从JDK1.5开始,加强型for循环也可以实现集合的输出了。这种输出的形式与数组的输出操作形式类似:
import java.util.ArrayList; import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Hello"); list.add("World"); list.add("Pikaqiu"); for (String str: list) { System.out.println(str); } // 输出: // Hello // World // Pikaqiu } }
这种输出最初出现时,很多人并不建议使用,因为标准的集合操作还是应该以Iterator为主,但毕竟JDK1.5都已经推出十多年了,很多语法也开始被大部分人所习惯。
第32章:Map集合
课时139:Map接口简介
-
Collection接口保存的数据:单个对象;Map接口保存的数据:二元偶对象(key=value),而存储二元偶对象的核心意义在于,需要通过key来获取对应的value。
-
Collection集合保存数据的目的:为了输出;Map集合保存数据的目的:为了进行key的查找。
-
Map接口时进行二元偶对象保存的最大父接口,接口定义如下:
public interface Map<K,V>
-
Map接口的核心操作方法:
No 方法 类型 描述 1 public V put(K key,V value) 普通 保存数据 2 public V get(Object key) 普通 获取数据 3 public Set<Map.Entry<K,V>> entrySet() 普通 将Map转为Set 4 public boolean containsKey(Object key) 普通 判断key是否存在 5 public Set keySet() 普通 将所有的key转为Set 6 public V remove(Object key) 普通 根据指定key删除数据 -
示例:
import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = Map.of("one", 1, "two", 2); // {one=1, two=2} // Map<String, Integer> map = Map.of("one", 1, "two", 2, "one", 11); // 抛出异常 // 抛出异常:Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: one // Map<String, Integer> map = Map.of("one", 1, "two", 2, null, 11); // 抛出异常 // 抛出异常:Exception in thread "main" java.lang.NullPointerException System.out.println(map); } }
在Map的of()方法中:
- key有重复:会抛出java.lang.IllegalArgumentException的异常
- key为null:会抛出java.lang.NullPointerException的异常
of()方法并不是标准用法,正常的开发中需要通过子类进行接口对象的实例化。
-
Map接口的常用子类:
- HashMap
- HashTable
- TreeMap
- LinkedHashMap
课时140:HashMap子类
-
HashMap是Map接口中最为常见的一个子类,其主要特点是无序存储。
-
HashMap类的定义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
-
示例:
import java.util.HashMap; import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("one", 1); map.put("two", 2); map.put("one", 11); // key重复 map.put(null, 0); // key为null map.put("zero", null); // value为null System.out.println(map); // 输出: // {null=0, zero=null, one=11, two=2} } }
put()方法会返回该key原来的value:
import java.util.HashMap; import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); System.out.println(map.put("one", 1)); // 输出:null System.out.println(map.put("one", 11)); // 输出:1 } }
-
put()方法的源码分析:
在使用put()方法进行数据保存时,会调用一个putVal()方法,同时会将key进行hash处理(生成一个hash码);而在putVal()方法中,会提供一个Node节点类进行数据的保存,并会调用一个resize()方法进行容量的扩充。
-
-
面试题:在进行HashMap的put()操作时,如何实现容量扩充?
- 初始化容量DEFAULT_INITIAL_CAPACITY为16:
- 在保存数据时,如果容量超过了临界值(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY = 0.75 * 16 = 12),就会进行容量的扩充:
- 在进行扩充时,采用的也是成倍增长的形式,即:每一次都扩充2倍的容量。
-
面试题:请解释HashMap的工作原理?
- 在HashMap中,依然是利用Node类来进行数据存储,所以其可以使用的数据结构只有:
- 链表(时间复杂度O(n))
- 二叉树(时间复杂度O(logn));
- 从JDK1.8开始,HashMap的实现出现了改变,因为要适应大数据(数据量小时,链表和红黑树的性能差别不大),所以其存储发生了变化;
- 在使用HashMap进行数据保存时:如果保存数据的容量没有超过TREEIFY_THRESHOLD(8),那么会按链表的形式进行存储;如果超过了,则会将链表转为红黑树以实现树的平衡,并利用左旋与右旋保证数据的查询性能。
- 在HashMap中,依然是利用Node类来进行数据存储,所以其可以使用的数据结构只有:
课时141:LinkedHashMap子类
-
HashMap虽然是Map集合中最为常用的子类,但其保存的数据都是无序的(有序与否对Map没有影响),如果现在希望Map集合中保存的数据为有序的,则可以使用LinkedHashMap(基于链表实现的)。
-
LinkedHashMap类的定义:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
因为是用链表保存,所以在使用LinkedHashMap类时,数据量不要很大,否则会造成时间复杂度攀升。
-
示例:
import java.util.LinkedHashMap; import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new LinkedHashMap<>(); map.put("one", 1); map.put("two", 2); map.put("one", 11); // key重复 map.put(null, 0); // key为null map.put("zero", null); // value为null System.out.println(map); // 输出: // {one=11, two=2, null=0, zero=null} } }
课时142:Hashtable子类
-
Hashtable类是从JDK1.0就提供了的,与Vector、Enumeration为最早的一批动态数组实现类,并且是最早的key-value接口,后来为了将其继续保留下来,让其多实现了一个Map接口。
-
Hashtable类的定义:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable
-
示例:
import java.util.Hashtable; import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new Hashtable<>(); map.put("one", 1); map.put("two", 2); map.put("one", 11); // key重复 // map.put(null, 0); // key为null,抛出异常 // 抛出异常:java.lang.NullPointerException // map.put("zero", null); // value为null,抛出异常 // 抛出异常:java.lang.NullPointerException System.out.println(map); // 输出: // {two=2, one=11} } }
可以发现,在Hashtable中保存数据时,设置的key或value都不允许为null,否则会出现NullPointerException异常。
-
面试题:请解释HashMap与Hashtable的区别?
- HashMap中的方法都是异步方法(非线程安全),而Hashtable中的方法都是同步方法(线程安全);
- HashMap允许保存有null的数据,而Hashtable不允许保存null,否则会出现NullPointerException异常。
课时143:Map.Entry内部接口
-
在LinkedList中,数据存储依靠的是链表,会把数据存储到Node节点中;而HashMap中的Node内部类实现了Map.Entry的内部接口:
static class Node<K,V> implements Map.Entry<K,V>
-
Map.Entry接口的定义:
public static interface Map.Entry<K,V>
在JDK1.9之前,基本都不会考虑创建Map.Entry对象;但在JDK1.9之后,Map接口新增了entry()方法。
-
创建Map.Entry对象:
static <K,V> Map.Entry<K,V> entry (K k, V v)
-
-
Map.Entry接口的方法:
- 获取key: K getKey()
- 获取value: V getValue()
-
示例:
import java.util.Map; public class JavaDemo{ public static void main(String[] args) { Map.Entry<String, Integer> entry = Map.entry("one", 1); System.out.println("key = " + entry.getKey()); System.out.println("value = " + entry.getValue()); System.out.println(entry.getClass().getName()); // 输出: // key = one // value = 1 // java.util.KeyValueHolder } }
-
在Map集合里,Map.Entry就是作为Key+Value的包装类。在数据存储时,都会把key和value包装为一个Map.Entry对象来使用。
课时144:迭代输出Map集合
-
迭代输出Map集合有两种方式:
- 利用Iterator
- 利用foreach
虽然Map集合可以进行迭代输出,但它主要的用法在于进行key的查找。
-
利用Iterator输出Map集合(集合的标准输出):
-
集合的标准输出:利用Iterator接口进行循环输出。但Map接口中并没有方法可以直接返回Iterator对象。
-
Map集合中提供的方法:
-
将Map集合转成Set集合: Set<Map.Entry<K,V>> entrySet()
-
-
利用Iterator输出Map集合的步骤:
- 利用Map接口中的entrySet()方法,将Map集合转为Set集合;
- 利用Set接口中的iterator()方法,将Set集合转为Iterator接口实例;
- 利用Iterator进行迭代输出,获取每一组的Map.Entry对象。
-
示例:
import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("one", 1); map.put("two", 2); map.put("three", 3); // 1. 将Map集合转为Set集合 Set<Map.Entry<String, Integer>> set = map.entrySet(); // 2. 将Set集合转为Iteratpr接口实例 Iterator<Map.Entry<String, Integer>> iterator = set.iterator(); // 3. 利用Iterator进行迭代输出 while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); System.out.println(entry.getKey() + ":" + entry.getValue()); } // 输出: // one:1 // two:2 // three:3 } }
-
-
利用foreach输出Map集合(集合的非标准输出):
import java.util.HashMap; import java.util.Map; import java.util.Set; public class JavaDemo{ public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("one", 1); map.put("two", 2); map.put("three", 3); // 1. 将Map集合转为Set集合 Set<Map.Entry<String, Integer>> set = map.entrySet(); // 2. 利用foreach进行迭代输出 for (Map.Entry<String, Integer> entry : set) { System.out.println(entry.getKey() + ":" + entry.getValue()); } // 输出: // one:1 // two:2 // three:3 } }
课时145:自定义Map的key类型
-
Map集合的key使用自定义类:
import java.util.HashMap; import java.util.Map; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } } public class JavaDemo{ public static void main(String[] args) { { Map<String, Person> map = new HashMap<>(); map.put("林弱", new Person("林强", 20)); System.out.println(map); System.out.println(map.get("林弱")); // 输出: // {林弱=姓名:林强,年龄:20} // 姓名:林强,年龄:20 } System.out.println("————————————"); { Map<Person, String> map = new HashMap<>(); Person linqiang = new Person("林强", 20); map.put(linqiang, "林弱"); System.out.println(map); System.out.println(map.get(linqiang)); // 输出: // {姓名:林强,年龄:20=林弱} // 林弱 } System.out.println("————————————"); { Map<Person, String> map = new HashMap<>(); map.put(new Person("林强", 20), "林弱"); System.out.println(map); System.out.println(map.get(new Person("林强", 20))); // 输出: // {姓名:林强,年龄:20=林弱} // null } } }
-
Map集合的源码分析:
-
put()方法源码:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
可以发现,在保存数据时,利用hash()方法生成了一个hash码,而hash()方法中又调用了Object类的hashCode()方法。
-
get()方法源码:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
而在获取数据时,也仍然使用到了hash码。
继续深入getNode()方法查找数据时,可以发现其调用了key的equals()方法。
-
所以,如Map集合中的key为自定义类时,这个类中需要重写hashCode()与equals()方法,如此,便可以根据自定义类的key查找到对应数据。
-
-
重写自定义类的hashCode()与equals()方法(果断编辑器直接生成):
import java.util.HashMap; import java.util.Map; import java.util.Objects; class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "姓名:" + this.name + ",年龄:" + this.age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } public class JavaDemo{ public static void main(String[] args) { Map<Person, String> map = new HashMap<>(); map.put(new Person("林强", 20), "林弱"); System.out.println(map); System.out.println(map.get(new Person("林强", 20))); // 输出: // {姓名:林强,年龄:20=林弱} // 林弱 } }
-
虽然Map集合中可以使用自定义类作为key的类型,但常用的key类型就是String、Long、Integer,应该尽量使用系统类。
-
面试题:HashMap是如何解决在进行数据操作时出现的Hash冲突(Hash码相同)?
-
当出现Hash冲突,为了保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表。
-
第33章:集合工具类
课时146:Stack栈操作
-
栈:先进后出的数据结构。
-
栈的基本操作形式:
-
在Java中,使用Stack来描述栈的操作,Stack类的定义:
public class Stack<E> extends Vector<E>
Stack类是Vector类的子类,但它并没有使用Vector类的方法。
-
Stack类的方法:
- 入栈:public E push(E item)
- 出栈:public E pop()
-
示例:
import java.util.Stack; public class JavaDemo{ public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.push("A"); stack.push("B"); stack.push("C"); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); System.out.println(stack.pop()); // 输出: // C // B // A // Exception in thread "main" java.util.EmptyStackException } }
课时147:Queue队列
-
队列:先进先出的数据结构。
-
队列的基本操作形式:
-
在Java中,使用Queue来描述队列的操作,Queue接口的定义:
public interface Queue<E> extends Collection<E>
-
Queue接口的方法:
- 追加数据:
- public boolean offer(E e) → 实际上调用的也是add()方法
- public boolean add(E e)
- 弹出数据:public E poll()
- 追加数据:
-
示例:
import java.util.LinkedList; import java.util.PriorityQueue; import java.util.Queue; public class JavaDemo{ public static void main(String[] args) { // 方式一:使用LinkedList子类来实现Queue接口 // Queue<String> queue = new LinkedList<>(); // 方式二:使用PriorityQueue优先级队列子类来实现Queue接口 Queue<String> queue = new PriorityQueue<>(); queue.offer("A"); queue.offer("B"); queue.offer("C"); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); System.out.println(queue.poll()); // 输出: // A // B // C // null } }
课时148:Properties属性操作
-
国际化程序中使用的资源文件(*.properties),其存储结构与Map集合类似,但其保存的只能是字符串。所以为了方便属性的描述,java.util包中提供了Properties类。
-
Properties类的定义:
public class Properties extends Hashtable<Object,Object>
-
Properties类的方法:
No 方法 类型 描述 1 public Object setProperty(String key, String value) 普通 设置属性 2 public String getProperty(String key) 普通 获取属性,key不存在时返回null 3 public String getProperty(String key, String defaultValue) 普通 获取属性,key不存在时返回默认值 4 public void store(OutputStream out, String comments) throws IOException 普通 通过输出流输出属性内容 5 public void load(InputStream inStream) throws IOException 普通 通过输入流读取属性内容 -
示例:
import java.util.Properties; public class JavaDemo{ public static void main(String[] args) { Properties properties = new Properties(); properties.setProperty("name", "pikaqiu"); System.out.println(properties.getProperty("name")); System.out.println(properties.getProperty("age")); System.out.println(properties.getProperty("age", "12")); // 输出: // pikaqiu // null // 12 } }
Properties类可以像Map集合那样进行内容的设置和获取,但它只能操作String类型。
-
Properties类最重要的功能:可以通过输出流输出属性、通过输入流读取属性:
-
保存文件:
import java.io.File; import java.io.FileOutputStream; import java.util.Properties; public class JavaDemo{ public static void main(String[] args) throws Exception { Properties properties = new Properties(); properties.setProperty("name", "pikaqiu"); properties.setProperty("age", "16"); String fileName = "D:" + File.separator + "test" + File.separator + "info.properties"; properties.store(new FileOutputStream(new File(fileName)), "pikaqiu-info"); } }
info.properties文件内容:
#pikaqiu-info #Sun Feb 14 22:11:48 CST 2021 name=pikaqiu age=16
-
读取文件:
import java.io.File; import java.io.FileInputStream; import java.util.Properties; public class JavaDemo{ public static void main(String[] args) throws Exception { Properties properties = new Properties(); String fileName = "D:" + File.separator + "test" + File.separator + "info.properties"; properties.load(new FileInputStream(new File(fileName))); System.out.println(properties.getProperty("name")); System.out.println(properties.getProperty("age")); // 输出: // pikaqiu // 16 } }
ResourceBundle一般读取信息资源,Properties一般读取配置资源,主要在程序初始化准备时使用。
-
课时149:Collections工具类
-
Collections:是Java提供的操作集合数据的工具类,也就是利用它可以实现各个集合的操作。
-
示例:
-
使用Collections操作List集合:
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class JavaDemo{ public static void main(String[] args) { List<String> list = new ArrayList<>(); Collections.addAll(list, "Hello", "World", "Pikaqiu"); System.out.println(list); // 输出:[Hello, World, Pikaqiu] Collections.reverse(list); // 实现反转 System.out.println(list); // 输出:[Pikaqiu, World, Hello] Collections.sort(list); // 实现排序 System.out.println(list); // 输出:[Hello, Pikaqiu, World] // binarySearch():二分查找,查找前需要先sort System.out.println(Collections.binarySearch(list, "World")); // 输出:2 } }
-
-
面试题:请解释Collection与Collections的区别?
- Collection:是集合接口;
- Collections:是操作集合的工具类。
第34章:Stream数据流
课时150:Stream基本操作
-
从JDK1.8开始,进入到了大数据时代,所以在类集里也支持有数据的流式分析处理操作,为此就专门提供了Stream的接口,同时在Collection接口里也提供有为此接口实例化的方法:
- 获取并行的Stream接口对象:default Stream parallelStream()
- 获取Stream接口对象:default Stream stream()
-
Stream主要功能:
- 进行数据的分析处理
- 主要针对集合中的数据进行分析处理
-
Stream接口的方法:
- 分页:
- 设置获取的最大数据量:public Stream limit(long maxSize)
- 跳过指定数据量进行读取:public Stream skip(long n)
- 分页:
-
示例:
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class JavaDemo{ public static void main(String[] args) { List<String> list = new ArrayList<>(); Collections.addAll(list, "Java", "JavaScript", "Ruby", "PHP", "Python", "GO", "Json", "JSP"); Stream<String> stream = list.stream(); // 获取Stream接口对象 // System.out.println(stream.count()); // 获取个数,返回的为long型 { // 通过函数式编程进行流式处理 // System.out.println(stream.filter( // (ele)->ele.toLowerCase().contains("j") // ).count()); // 输出:4 } { // 数据采集 // List<String> result = stream.filter( // (ele)->ele.toLowerCase().contains("j") // ).collect(Collectors.toList()); // System.out.println(result); // 输出:[Java, JavaScript, Json, JSP] } { // 数据分页,在采集之前分页 List<String> result = stream.filter( (ele)->ele.toLowerCase().contains("j") ).skip(2).limit(2).collect(Collectors.toList()); System.out.println(result); // 输出:[Json, JSP] } } }
课时151:MapReduce基础模型
-
MapReduce基础模型:
- Map:处理
- Reduce:分析
在数据分析之前,必须要对数据进行合理的处理,然后才可以做统计分析操作。
-
示例:
import java.util.ArrayList; import java.util.DoubleSummaryStatistics; import java.util.List; class Order { // 订单类 private String name; // 商品名称 private double price; // 商品价格 private int amount; // 商品数量 public Order(String name, double price, int amount) { this.name = name; this.price = price; this.amount = amount; } public String getName() { return this.name; } public double getPrice() { return this.price; } public int getAmount() { return this.amount; } } public class JavaDemo{ public static void main(String[] args) { List<Order> listOrder = new ArrayList<>(); listOrder.add(new Order("小强娃娃", 9.9, 10)); listOrder.add(new Order("林弱充气娃娃", 2987.9, 3)); listOrder.add(new Order("不强牌笔记本电脑", 8987, 8)); listOrder.add(new Order("弱强茶杯", 2.9, 800)); listOrder.add(new Order("阿强牌煎饼", 0.9, 138)); DoubleSummaryStatistics stat = listOrder.stream().filter( (ele)->ele.getName().contains("强") ).mapToDouble( (orderObject)->orderObject.getPrice() * orderObject.getAmount() ).summaryStatistics(); System.out.println("购买数量:" + stat.getCount()); System.out.println("购买总价:" + stat.getSum()); System.out.println("平均花费:" + stat.getAverage()); System.out.println("最高花费:" + stat.getMax()); System.out.println("最低花费:" + stat.getMin()); // 输出: // 购买数量:4 // 购买总价:74439.2 // 平均花费:18609.8 // 最高花费:71896.0 // 最低花费:99.0 System.out.println(stat); // 输出: // DoubleSummaryStatistics{count=4, sum=74439.200000, min=99.000000, average=18609.800000, max=71896.000000} } }
-
这些分析操作只是JDK本身提供的支持,但实际中并不这样进行操作,因为如果所有的数据都保存在内存中,而当数据量很大时,就崩掉了。
————————————————————————————————————
加餐:
加餐一:Collection接口的父子类关系图:
加餐二:Map接口的父子类关系图:
————————————————————————————————————