java基础,java语法,javase学习笔记,笔记发布进度2/3。java集合Collections类,java泛型,java IO流

11. Java之集合

11.1 集合与数组

11.1.1 集合与数组存储数据概述:

集合、数组都是对多个数据进行存储操作的结构,简称Java容器。 说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)

11.1.2 数组存储的特点:

一旦初始化以后,其长度就确定了。 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。

比如:String[] arrint[] arr1Object[] arr2

11.1.3 数组存储的弊端:

  1. 一旦初始化以后,其长度就不可修改。
  2. 数组中提供的方法非常限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  3. 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
  4. 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

11.1.4 集合存储的优点:

解决数组存储数据方面的弊端。

11.1.5 集合的分类

Java集合可分为Collection和Map两种体系

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • list:元素有序、可重复的集合
    • Set:元素无序、不可重复的集合
  • Map接口:双列数据,保存具有映射关系"key-value对"的集合

11.1.6 集合的框架结构

 |----Collection接口:单列集合,用来存储一个一个的对象
      |----List接口:存储有序的、可重复的数据。  -->“动态”数组
            |----ArrayList:作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData数组存储
            |----LinkedList:对于频繁的插入删除操作,使用此类效率比ArrayList效率高底层采用双向链表存储
            |----Vector:作为List的古老实现类,线程安全的,效率低;底层采用Object[]数组存储
            
      |----Set接口:存储无序的、不可重复的数据   -->数学概念上的“集合”
            |----HashSet:作为Set接口主要实现类;线程不安全;可以存null值
                 |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加顺序遍历;对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
            |----TreeSet:可以按照添加对象的指定属性,进行排序。
 
 
 |----Map:双列数据,存储key-value对的数据   ---类似于高中的函数:y = f(x)
      |----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
           |----LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历。
                     原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
                     对于频繁的遍历操作,此类执行效率高于HashMap。
      |----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
                       底层使用红黑树
      |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
           |----Properties:常用来处理配置文件。key和value都是String类型

11.2 Collection接口

  • Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。
  • JDK不提供此借口的任何实现,而是提供更具体的子接口(如:Set和List)实现。
  • 在JDK 5.0 之前,Java集合丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理;从JDK 5.0 增加了泛型以后,Java集合可以记住容器中对象的数据类型。

11.2.1 单列集合框架结构

 |----Collection接口:单列集合,用来存储一个一个的对象
      |----List接口:存储有序的、可重复的数据。  -->“动态”数组
            |----ArrayList:作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData数组存储
            |----LinkedList:对于频繁的插入删除操作,使用此类效率比ArrayList效率高底层采用双向链表存储
            |----Vector:作为List的古老实现类,线程安全的,效率低;底层采用Object[]数组存储
            
      |----Set接口:存储无序的、不可重复的数据   -->数学概念上的“集合”
            |----HashSet:作为Set接口主要实现类;线程不安全;可以存null值
                 |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加顺序遍历;对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
            |----TreeSet:可以按照添加对象的指定属性,进行排序。

图示:

image-20220411083433886

11.2.2 Collection接口常用方法

  1. 添加
    • add(Object obj)
    • addAll(Collection coll)
  2. 获取有效元素个数
    • int size()
  3. 清空集合
    • void clear()
  4. 是否为空集合
    • boolean isEmpty()
  5. 是否包含某个元素
    • boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象。
    • boolean contains(Collection c):也是调用元素的equals方法来比较的。用两个集合的元素逐一比较。
  6. 删除
    • boolean remove(Object obj):通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素。
    • boolean removeAll(Collection coll):取当前集合的差集
  7. 取两个集合的交集
    • boolean retainAll(Collection c):把交集的结果存在当前的集合中,不影响c
  8. 集合是否相等
    • boolean equals(Object obj)
  9. 转换成对象数组
    • Object [] 同Array()
  10. 获取集合对象的哈希值
 -   `hashCode()`
  1. 遍历
 -   `iterator()`:返回迭代器对象,用于集合遍历

代码示例:

 @Test
 public void test1() {
     Collection collection = new ArrayList();
     //1.add(Object e):将元素添加到集合中
     collection.add("ZZ");
     collection.add("AA");
     collection.add("BB");
     collection.add(123);
     collection.add(new Date());
     //2.size():获取添加元素的个数
     System.out.println(collection.size());//5
     //3.addAll(Collection coll1):将coll1集合中的元素添加到当前集合中
     Collection collection1 = new ArrayList();
     collection1.add("CC");
     collection1.add(213);
     collection.addAll(collection1);
     System.out.println(collection.size());//9
     //调用collection1中的toString()方法输出
     System.out.println(collection);//[ZZ, AA, BB, 123, Tue Apr 28 09:22:34 CST 2020, 213, 213]
     //4.clear():清空集合元素
     collection1.clear();
     System.out.println(collection1.size());//0
     System.out.println(collection1);//[]
     //5.isEmpty():判断当前集合是否为空
     System.out.println(collection1.isEmpty());//true
 }
 
 @Test
 public void test2() {
     Collection coll = new ArrayList();
     coll.add(123);
     coll.add(456);
     coll.add(new Person("Tom", 23));
     coll.add(new Person("Jarry", 34));
     coll.add(false);
     //6.contains(Object obj):判断当前集合中是否包含obj
     //判断时需要调用obj对象所在类的equals()方法
     System.out.println(coll.contains(123));//true
     System.out.println(coll.contains(new Person("Tom", 23)));//true 此时已经对Person类对象进行equals contain使用equals比较,equals变成比较内容而不是地址
     System.out.println(coll.contains(new Person("Jarry", 23)));//false
     //7.containsAll(Collection coll1):判断形参coll1中的元素是否都存在当前集合中
     Collection coll1 = Arrays.asList(123, 4566);
     System.out.println(coll.containsAll(coll1));//flase
     //8.remove(Object obj):从当前集合中移除obj元素
     coll.remove(123);
     System.out.println(coll);//[456, Person{name='Tom', age=23}, Person{name='Jarry', age=34}, false]
     //9.removeAll(Collection coll1):差集:从当前集合中和coll1中所有的元素
     Collection coll2 = Arrays.asList(123, 456, false);
     coll.removeAll(coll2);
     System.out.println(coll);//[Person{name='Tom', age=23}, Person{name='Jarry', age=34}]
 }
 
 @Test
 public void test3() {
     Collection coll = new ArrayList();
     coll.add(123);
     coll.add(456);
     coll.add(new Person("Tom", 23));
     coll.add(new Person("Jarry", 34));
     coll.add(false);
     //10.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
     Collection coll1 = Arrays.asList(123, 345, 456);
     boolean b = coll.retainAll(coll1);
     System.out.println(b);//true
     System.out.println(coll);//[123, 456]
     //11.equals(Object obj):返回true需要当前集合和形参集合的元素相同
     Collection coll2 = new ArrayList();
     coll2.add(123);
     coll2.add(456);
     System.out.println(coll.equals(coll2));//true
     //12.hashCode():返回当前对象的哈希值
     System.out.println(coll.hashCode());//5230
     //13.集合--->数组:toArray()
     Object[] array = coll.toArray();
     for (Object obj : array) {
         System.out.println(obj);
     }
     //14.数组--->集合:调用Arrays类的静态方法asList()
     List<int[]> ints = Arrays.asList(new int[]{123, 345});
     System.out.println(ints.size());//1
     List<String> strings = Arrays.asList("AA", "BB", "CC");
     System.out.println(strings);//[AA, BB, CC]
     //15.iteratoriterator():返回Iterator接口的实例,用于遍历集合元素。
 }

11.2.3 Collection集合与数组间的转换

 //集合 --->数组:toArray()
 Object[] arr = coll.toArray();
 for(int i = 0;i < arr.length;i++){
     System.out.println(arr[i]);
 }
 
 //拓展:数组 --->集合:调用Arrays类的静态方法asList(T ... t)
 List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
 System.out.println(list);
 
 List arr1 = Arrays.asList(new int[]{123, 456});
 System.out.println(arr1.size());//1
 
 List arr2 = Arrays.asList(new Integer[]{123, 456});
 System.out.println(arr2.size());//2

使用Collection 集合存储对象,要求对象所属的类满足:

向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()

11.3 Iterator接口与foreach循环

11.3.1 遍历Collection的两种方式:

  1. 使用迭代器Iterator
  2. foreach循环(或增强for循环)

11.3.2 java.utils包下定义的迭代器接口:Iterator

11.3.2.1 说明:

Iterator对象统称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素。GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的细节。迭代器模式,就是为容器而生。

11.3.2.2 作用

遍历集合Collection元素

11.3.2.3 如何获取实例

coll.iterator() 返回一个迭代器实例

11.3.2.4 遍历的代码实现:
 Iterator iterator = coll.iterator();//hasNext():判断是否还下一个元素while(iterator.hasNext()){    //next():①指针下移 ②将下移以后集合位置上的元素返回    System.out.println(iterator.next());}
11.3.2.5 图示说明:

image-20220411083453433

11.3.2.6 Iterator中remove()方法的使用
  • 测试Iterator中的remove()
  • 如果还未调用next() 或在上一次调用next() 方法之后就已经调用了remove() 方法,再调用remove都会报IllegalStateException
  • 内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove()

代码示例:

 @Testpublic void test3(){    Collection coll = new ArrayList();    coll.add(123);    coll.add(456);    coll.add(new Person("Jerry",20));    coll.add("Tom"            );    coll.add(false);    //删除集合中"Tom"    Iterator iterator = coll.iterator();    while (iterator.hasNext()){        //            iterator.remove();        Object obj = iterator.next();        if("Tom".equals(obj)){            iterator.remove();            //                iterator.remove();        }    }    //将指针重新放到头部,遍历集合    iterator = coll.iterator();    while (iterator.hasNext()){        System.out.println(iterator.next());    }}

11.3.3 增强for循环:(foreach循环)

JDK 5.0 新特性

11.3.3.1 遍历集合举例:
 @Testpublic void test1(){    Collection coll = new ArrayList();    coll.add(123);    coll.add(456);    coll.add(new Person("Jerry",20));    coll.add(new String("Tom"));    coll.add(false);    //for(集合元素的类型 局部变量 : 集合对象)        for(Object obj : coll){        System.out.println(obj);    }}

说明:内部仍然调用了迭代器

11.3.3.2 遍历数组举例:
 @Test
 public void test2(){
     int[] arr = new int[]{1,2,3,4,5,6};
     //for(数组元素的类型 局部变量 : 数组对象)
     for(int i : arr){
         System.out.println(i);
     }
 }

11.4 Collection子接口:List接口

11.4.1 存储的数据特点:

存储序有些的、可重复的数据。

  • 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • List容器中的元素都对应一个整型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。

11.4.2 常用方法

List除了从Collection集合继承的方法之外,List集合里添加了一些根据索引来操作集合元素的方法。

  • void add(int index,Object ele):在index位置插入ele元素
  • boolean addAll(Int index,Collection eles):从index位置开始将eles中的所有元素添加进来。
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index,Object ele):设置指定index位置的元素为ele
  • List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合。

总结:

  • 增:add(Object obj)
  • 删:remove(int index) / remove(Object obj)
  • 改:set(int index,Object ele)
  • 查:get(int index)
  • 插:add(int index,Object ele)
  • 长度:size()
  • 遍历:1.Iterator迭代器方式 2. foreach(增强for循环) 3.普通的循环

代码示例:

 @Test
 public void test2(){
     ArrayList list = new ArrayList();
     list.add(123);
     list.add(456);
     list.add("AA");
     list.add(new Person("Tom",12));
     list.add(456);
     //int indexOf(Object obj):返回obj在集合中首次出现的位置。如果不存在,返回-1.
     int index = list.indexOf(4567);
     System.out.println(index);
 
     //int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。如果不存在,返回-1.
     System.out.println(list.lastIndexOf(456));
 
     //Object remove(int index):移除指定index位置的元素,并返回此元素
     Object obj = list.remove(0);
     System.out.println(obj);
     System.out.println(list);
 
     //Object set(int index, Object ele):设置指定index位置的元素为ele
     list.set(1,"CC");
     System.out.println(list);
 
     //List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的左闭右开区间的子集合
     List subList = list.subList(2, 4);
     System.out.println(subList);
     System.out.println(list);
 }
 
 
 @Test
 public void test1(){
     ArrayList list = new ArrayList();
     list.add(123);
     list.add(456);
     list.add("AA");
     list.add(new Person("Tom",12));
     list.add(456);
 
     System.out.println(list);
 
     //void add(int index, Object ele):在index位置插入ele元素
     list.add(1,"BB");
     System.out.println(list);
 
     //boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
     List list1 = Arrays.asList(1, 2, 3);
     list.addAll(list1);
     //        list.add(list1);
     System.out.println(list.size());//9
 
     //Object get(int index):获取指定index位置的元素
     System.out.println(list.get(0));
 
 }

11.4.3 常用实现类

 3. 常用实现类:
 |----Collection接口:单列集合,用来存储一个一个的对象
   |----List接口:存储序的、可重复的数据。  -->“动态”数组,替换原的数组
       |----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
       |----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
       |----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

11.4.3.1 ArrayList

  • ArrayList是List接口的典型实现类、主要实现类
  • 本质上,ArrayList是对象引用的一个”变长”数组
  • Array Listi的JDK 1.8之前与之后的实现区别?
    • JDK 1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
    • JDK 1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个初始容量为10的数组
  • Arrays.asList(…) 方法返回的List集合,既不是ArrayList实例,也不是Vector实例。
  • Arrays.asList(…) 返回值是一个固定长度的List集合
 @Test
 public void test1() {
     Collection coll = new ArrayList();
     coll.add(123);
     coll.add(345);
     coll.add(new User("Tom", 34));
     coll.add(new User("Tom"));
     coll.add(false);
     //iterator()遍历ArrayList集合
     Iterator iterator = coll.iterator();
     while (iterator.hasNext()) {
         System.out.println(iterator.next());
     }
 }
11.4.3.2 linkedList
  • 对与对于频繁插入和删除元素操作,建议使用LinkedList类,效率更高
  • 新增方法:
    • void addFirst(Object obj)
    • void addLast(Object obj)
    • Object getFirst()
    • Object getLast()
    • Object removeFirst()
    • Object removeLast()
  • LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的FIrst和last,用于记录首末元素。同时,定义内部Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
    • prev:变量记录前一个元素的位置
    • next:变量记录下一个元素的位置

image-20220411083504846

代码示例:

 @Test
 public void test3(){
     LinkedList linkedList = new LinkedList();
     linkedList.add(123);
     linkedList.add(345);
     linkedList.add(2342);
     linkedList.add("DDD");
     linkedList.add("AAA");
     
     Iterator iterator = linkedList.iterator();
     while (iterator.hasNext()){
         System.out.println(iterator.next());
     }
 }

11.4.4 源码分析(难点)

11.4.4.1 ArrayList的源码分析

JDK 7.0 情况下

 ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
 list.add(123);//elementData[0] = new Integer(123);
 ...
 list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
  • 默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
  • 结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

JDK 8.0中ArrayList的变化

 ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
 list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
 ...

后续的添加和扩容操作与JDK 7.0 无异。

小结

JDK 7.0 中的ArrayList的对象的创建类似于的单例的饿汉模式,而JDK 8.0 中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

11.4.4.2 LinkedList的源码分析:
 LinkedList list = new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null
 list.add(123);//将123封装到Node中,创建了Node对象。
 
 //其中,Node定义为:体现了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;
     }
 }
11.4.4.3 Vector的源码分析:
  • Vector是一个古老的集合,JDK 1.0 就有了。大多数操作与ArrayList相同,区别在于Vector是线程安全的。
  • 在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免选择使用。
  • JDK 7.0 和JDK 8.0 中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
  • 在扩容方面,默认扩容为原来数组长度的2倍。

11.4.5 存储的元素要求:

添加的对象,所在的类要重写equals()方法

11.4.6 面试题

请问ArrayList / LinkedList / Vector 的异同?谈谈你的理解?ArrayList底层是什么? 扩容机制? Vector和 ArrayList的最大区别?

  • ArrayList 和LinkedList 的异同:

    二者都线程不安全,相比线程安全的Vector,ArrayList执行效率。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。

  • ArrayList和Vector的区别:

    Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类,因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

11.5 Collection子接口:Set接口概述

  • Set接口是Collection的子接口,Set没有额外的方法
  • Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败。(多用于过滤操作,去掉重复数据)
  • Set判断两个对象是否相同不是使用==运算符,而是根据equals()方法

11.5.1 存储的数据特点:

用于存放无序的、不可重复的元素

以HashSet为例说明:

  1. 无序性:不等于随机性。存储的数据在底层的数组中并非数组索引的顺序添加,而是根据数据的哈希值决定的。
  2. 不可重复性:保证添加的元素照equals()判断时,不能返回true,即:相同的元素只能添加一个。

11.5.2 元素添加过程:(以HashSet为例)

我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值 接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:

  • 如果此位置上没有其他元素,则元素a添加成功。—>情况1
  • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
    • 如果hash值不相同,则元素a添加成功。—>情况2
    • 如果hash值相同,进而需要调用元素a所在类的equals()方法:
      • equals()返回true,元素a添加失败
      • equals()返回false,则元素a添加成功。—>情况3

对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上数据以链表的方式存储。

JDK 7.0 :元素a放到数组中,指向原来的元素。

JDK 8.0 :原来的元素在数组中,指向元素a

总结:七上八下

HashSet底层:数组+链表的机构。(JDK 7.0 以前)

image-20220411083518372

11.5.3 常用方法

Set接口中没有额外定义新的方法,使用的都是Collction中声明过的方法。

11.5.3.1 重写hashCode()的基本方法
  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回true时,这两个对象的 hashCode() 方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的Field,都应该用来计算hashCode值。
11.5.3.2 重写equals() 方法基本原则
  • 以自定义的Customer类为例 ,何时需要重写equals()
  • 当一个类有自己特有的"逻辑相等"概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的示例可能在逻辑上是相等的,但是,根据Object.hashCode()方法,他们仅仅是两个对象。
  • 因此,违反了相等的对象必须要具有相等的散列码
  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
11.5.3.3 Eclipse/IDEA工具里hashCode()重写

以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals()hashCode()

问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的"冲突"就越少,查找起来效率也会提高。(减少冲突)
  • 并且31只占用5bits,相乘造成数据溢出的概率较小。
  • 31可以由i*31==(<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
  • 31是一个素数,素数的作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

代码示例:

 @Override
 public boolean equals(Object o) {
     System.out.println("User equals()....");
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
 
     User user = (User) o;
 
     if (age != user.age) return false;
     return name != null ? name.equals(user.name) : user.name == null;
 }
 
 @Override
 public int hashCode() { //return name.hashCode() + age;
     int result = name != null ? name.hashCode() : 0;
     result = 31 * result + age;
     return result;
 }

11.5.4 常用实现类:

  |----Collection接口:单列集合,用来存储一个一个的对象
       |----Set接口:存储无序的、不可重复的数据   -->高中讲的“集合”
            |----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
                 |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历,对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
            |----TreeSet:可以按照添加对象的指定属性,进行排序。
11.5.4.1 HashSet
  • HashSet是Set接口的典型实现,大多数时候使用Set集合时都使用这个实现类。
  • HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
  • HashSet具有以下特点:
    • 不能保证元素的顺序排序
    • HashSet不是线程安全的。
    • 集合元素可以是null
  • HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等。
  • 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”

代码示例:

 @Test
 //HashSet使用
 public void test1(){
     Set set = new HashSet();
     set.add(454);
     set.add(213);
     set.add(111);
     set.add(123);
     set.add(23);
     set.add("AAA");
     set.add("EEE");
     set.add(new User("Tom",34));
     set.add(new User("Jarry",74));
 
     Iterator iterator = set.iterator();
     while (iterator.hasNext()){
         System.out.println(iterator.next());
     }
 }
11.5.4.2 LinkedHashSet
  • LinkedHashSet是HashSet的子类
  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。
  • LinkedHashSet不允许集合元素重复。

图示:

image-20220411083527913

代码示例:

 @Test
 //LinkedHashSet使用
 public void test2(){
     Set set = new LinkedHashSet();
     set.add(454);
     set.add(213);
     set.add(111);
     set.add(123);
     set.add(23);
     set.add("AAA");
     set.add("EEE");
     set.add(new User("Tom",34));
     set.add(new User("Jarry",74));
 
     Iterator iterator = set.iterator();
     while (iterator.hasNext()){
         System.out.println(iterator.next());
     }
 }
11.5.4.3 TreeSet
  • TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。
  • TreeSet底层使用红黑树结构存储数据
  • 新增的方法如下:(了解)
    • Comparator comparator()
    • Object first()
    • Object last()
    • Object lower(Object e)
    • Object higher(Object e)
    • SortedSet subSet(fromElement ,toElement)
    • SortedSet headSet(toElement)
    • SortedSet tailSet(fromElement)
  • TreeSet两种排序方法:自然排序和定制排序。默认情况下,TreeSet采用自然排序。

红黑树图示:

image-20220411083537152

红黑树的特点:有序,查询效率比List快。

详细介绍:https://www.cnblogs.com/LiaHon/p/11203229.html

代码示例:

 @Test
 public void test1(){
     Set treeSet = new TreeSet();
     treeSet.add(new User("Tom",34));
     treeSet.add(new User("Jarry",23));
     treeSet.add(new User("mars",38));
     treeSet.add(new User("Jane",56));
     treeSet.add(new User("Jane",60));
     treeSet.add(new User("Bruce",58));
 
     Iterator iterator = treeSet.iterator();
     while (iterator.hasNext()){
         System.out.println(iterator.next());
     }
 }

11.5.5 存储对象所在类的要求

11.5.5.1 HashSet / LinkedHashSet
  • 要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
  • 要求:重写的hashCode()和equals()尽可能保持一致性;相等的对象必须具有相等的散列码

重写两个方法的小技巧:对象中用作equals()方法比较的Field,都应该用来计算hashCode值。

11.5.5.2 TreeSet:
  1. 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回0.不再是equals()
  2. 定制排序中,比较两个对象是否相同的标准为:compare() 返回0.不再是equals()

11.5.6 TreeSet的使用

11.5.6.1 使用说明:
  1. 向TreeSet中添加的数据,要求是相同类的对象。
  2. 两种排序方式:自然排序(实现Comparable接口和定制排序Comparator)
11.5.6.2 常用的排序方式:

方式一:自然排序

  • 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
  • 如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口。
    • 实现Comparable的类必须实现 compareTo(Object obj)方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小
  • Comparable的典型实现:
    • BigDecimal、BigInteger以及所有的数值型对应的包装类:按他们对应的数值大小进行比较
    • Charater:按字符的unicode值来进行比较
    • Boolean:true对应的包装类实例大于false对应的包装类实例
    • String:按字符串中字符的unicode值进行比较
    • Date、Time:后边的时间、日期比前面的时间、日期大
  • 向TreeSet中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
  • 因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象。对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较返回值。
  • 当需要把一个对象放入TreeSet中,重写该对象对应的equals()方法时,应该保证该方法与compareTo(Object obj)方法有一致的结果:如果两个对象通过equals()方法比较返回true,则通过compareTo(Object obj) 方法应比较返回0。否则,让人难以理解。
 @Test
 public void test1(){
     TreeSet set = new TreeSet();
 
     //失败:不能添加不同类的对象
     //        set.add(123);
     //        set.add(456);
     //        set.add("AA");
     //        set.add(new User("Tom",12));
 
     //举例一:
     //        set.add(34);
     //        set.add(-34);
     //        set.add(43);
     //        set.add(11);
     //        set.add(8);
 
     //举例二:
     set.add(new User("Tom",12));
     set.add(new User("Jerry",32));
     set.add(new User("Jim",2));
     set.add(new User("Mike",65));
     set.add(new User("Jack",33));
     set.add(new User("Jack",56));
 
     Iterator iterator = set.iterator();
     while(iterator.hasNext()){
         System.out.println(iterator.next());
     }
 
 }

方式二:定制排序

  • TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其他属性大小进行排序,则考虑使用定制。定制排序,通过Comparator接口实现。需要重写compare(T o1,T o2)方法。
  • 利用 int compare(T o1, T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0
 @Test
 public void test2(){
     Comparator com = new Comparator() {
         //照年龄从小到大排列
         @Override
         public int compare(Object o1, Object o2) {
             if(o1 instanceof User && o2 instanceof User){
                 User u1 = (User)o1;
                 User u2 = (User)o2;
                 return Integer.compare(u1.getAge(),u2.getAge());
             }else{
                 throw new RuntimeException("输入的数据类型不匹配");
             }
         }
     };
 
     TreeSet set = new TreeSet(com);
     set.add(new User("Tom",12));
     set.add(new User("Jerry",32));
     set.add(new User("Jim",2));
     set.add(new User("Mike",65));
     set.add(new User("Mary",33));
     set.add(new User("Jack",33));
     set.add(new User("Jack",56));
 
     Iterator iterator = set.iterator();
     while(iterator.hasNext()){
         System.out.println(iterator.next());
     }
 }

11.6 Map接口

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value
  • Map中的key和value都可以是任何引用类型的数据
  • Map中的key用set来存放,不允许重复,即同一个Map对象对应的类,须重写hashCode()equals()方法
  • 常用String类作为Map的"键"
  • key和value之间存在单向一对一关系,即通过指定的key总能找到唯一的、确定的value
  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是Map接口使用频率最高的实现类。

11.6.1 常见实现类结构

image-20220411083548736

 |----Map:双列数据,存储key-value对的数据   ---类似于高中的函数:y = f(x)
      |----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
           |----LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历。
                     原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
                     对于频繁的遍历操作,此类执行效率高于HashMap。
      |----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序
                       底层使用红黑树
      |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
           |----Properties:常用来处理配置文件。key和value都是String类型
 
 
 HashMap的底层: 数组+链表  (JDK 7.0及之前)
                数组+链表+红黑树 (JDK 8.0以后)
11.6.1.1 hashMap
  • HashMap是Map接口使用频率最高的实现类

  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。

  • 所有的key构成的集合set:无序、不可重复的。所以,key所在的类要重写equals()和hashCode()

  • 所有的value构成的集合是Collection:无序的、可重复的。所以,在value所在的类要重写:equals()

  • 一个key-value构成一个entry

  • 所有的entry构成的集合是Set:无序的、不可重复的

  • HashMap判断两个key相等的标准是:两个key通过equals()方法返回true,hashCode值也相等。

  • HashMap判断两个value相等的标准是:两个value通过equals()方法返回true。

代码示例:

 @Test
 public void test1(){
     Map map = new HashMap();
 
     map.put(null,123);
 
 }

jdk8 相较于jdk7在底层实现方面的不同

  1. new HashMap():底层没有创建一个长度为16的数组

  2. jdk8底层的数组是:Node[],而非Entry[]

  3. 首次调用put()方法时,底层创建长度为16的数组

  4. jdk7底层结构志愿:数组+链表。jdk8中底层结构:数组+链表+红黑树

    当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时

    此时此索引位置上的所有数据改为红黑树存储。

11.6.1.2 LinkedHashMap
  • LinkedHashMap底层使用的结构与Hash相同,因为LinkedHashMap继承于HashMap
  • 区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node
  • 与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序:迭代顺序与Key-Value的插入顺序一致。
 @Test
 public void test2(){
     Map map = new LinkedHashMap();
     map.put(123,"AA");
     map.put(345,"BB");
     map.put(12,"CC");
 
     System.out.println(map);
 } 
11.6.1.3 TreeMap
  • TreeMap存储Key-Value对时,需要根据key-value对进行排序。TreeMap可以保证所有的key-Value对处于有序状态。
  • TreeMap底层使用红黑树结构存储数据
  • TreeMap的key的排序
    • 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException
    • 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。此时不需要Map的key实现Comparable接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
11.6.1.4 Hashtable
  • Hashtable 是个古老的Map实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
  • 与HashMap不同,HashTable不允许使用null作为key和value。
  • 与HashMap一样,Hashtable也不能保证其中key-value对的顺序。
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
11.6.1.5 Properties
  • Properties类是Hashtable的子类,该对象用于处理属性文件‘
  • 由于属性文件里的key、value都是字符串类型,所以Properties里的key和value都是字符串类型
  • 存取数据时,建议使用setProperties(String key, String value)方法和getProperties(String key)方法

代码示例:

//Properties:常用来处理配置文件。key和value都是String类型
public static void main(String[] args)  {
    FileInputStream fis = null;
    try {
        Properties pros = new Properties();

        fis = new FileInputStream("jdbc.properties");
        pros.load(fis);//加载流对应的文件

        String name = pros.getProperty("name");
        String password = pros.getProperty("password");

        System.out.println("name = " + name + ", password = " + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

11.6.2 存储结构的理解

  • Map中的key无序的、不可重复的,使用Set存储所的key—>key所在的类要重写equals()和hashCode()(以HashMap为例)
  • Map中的value:无序的、可重复的,使用Collection存储所有的value—>value所在的类要重写equals()
  • 一个键值对:key-value构成一个Entry对象
  • Map中的entry:无序、不可重复的,使用Set存储所有的entry

image-20220411083559267

11.6.3 常用方法

11.6.3.1 添加、删除、修改操作:
  • Object put(Object key, Object value):将指定key-value添加到(或修改)当前map对象中
  • void putAll(Map m):将m中的所以key-value对存放到当前map中
  • Object remove(Object key):移除指定key的key-value对,并返回value
  • void clear():清空当前map中的所有数据

代码示例:

@Test
public void test1() {
    Map map = new HashMap();
    //Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
    map.put("AA",123);
    map.put("ZZ",251);
    map.put("CC",110);
    map.put("RR",124);
    map.put("FF",662);
    System.out.println(map);//{AA=123, ZZ=251, CC=110, RR=124, FF=662}

    //Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
    map.put("ZZ",261);
    System.out.println(map);//{AA=123, ZZ=261, CC=110, RR=124, FF=662}

    //void putAll(Map m):将m中的所有key-value对存放到当前map中
    HashMap map1 = new HashMap();
    map1.put("GG",435);
    map1.put("DD",156);
    map.putAll(map1);
    System.out.println(map);//{AA=123, ZZ=261, CC=110, RR=124, FF=662, GG=435, DD=156}

    //Object remove(Object key):移除指定key的key-value对,并返回value
    Object value = map.remove("GG");
    System.out.println(value);//435
    System.out.println(map);//{AA=123, ZZ=261, CC=110, RR=124, FF=662, DD=156}

    //void clear():清空当前map中的所有数据
    map.clear();
    System.out.println(map.size());//0  与map = null操作不同
    System.out.println(map);//{}
}
11.6.3.2 元素查询的操作
  • Object get(Object key):获取指定key对应的value
  • boolean containsKey(Object key):是否包含指定的key
  • boolean containsValue(Object value):是否包含指定的value
  • int size():返回map中key-value对的个数
  • boolean isEmpty():判断当前map是否为空
  • boolean equals(Object obj):判断当前map和参数对象obj是否相等

代码示例:

@Test
public void test2() {
    Map map = new HashMap();
    map.put("AA", 123);
    map.put("ZZ", 251);
    map.put("CC", 110);
    map.put("RR", 124);
    map.put("FF", 662);
    System.out.println(map);//{AA=123, ZZ=251, CC=110, RR=124, FF=662}
    //Object get(Object key):获取指定key对应的value
    System.out.println(map.get("AA"));//123

    //boolean containsKey(Object key):是否包含指定的key
    System.out.println(map.containsKey("ZZ"));//true

    //boolean containsValue(Object value):是否包含指定的value
    System.out.println(map.containsValue(123));//true

    //int size():返回map中key-value对的个数
    System.out.println(map.size());//5

    //boolean isEmpty():判断当前map是否为空
    System.out.println(map.isEmpty());//false

    //boolean equals(Object obj):判断当前map和参数对象obj是否相等
    Map map1 = new HashMap();
    map1.put("AA", 123);
    map1.put("ZZ", 251);
    map1.put("CC", 110);
    map1.put("RR", 124);
    map1.put("FF", 662);
    System.out.println(map.equals(map1));//true
}
11.6.3.3 元视图操作的方法
  • Set keySet():返回所有key构成的Set集合
  • Collection values():返回所有value构成的Collection集合
  • Set entrySet():返回所有key-value对构成的Set集合

代码示例:

@Test
public void test3() {
    Map map = new HashMap();
    map.put("AA", 123);
    map.put("ZZ", 251);
    map.put("CC", 110);
    map.put("RR", 124);
    map.put("FF", 662);
    System.out.println(map);//{AA=123, ZZ=251, CC=110, RR=124, FF=662}
    //遍历所有的key集:Set keySet():返回所有key构成的Set集合
    Set set = map.keySet();
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
    System.out.println("--------------");
    //遍历所有的value集:Collection values():返回所有value构成的Collection集合
    Collection values = map.values();
    for (Object obj :
         values) {
        System.out.println(obj);
    }
    System.out.println("---------------");
    //Set entrySet():返回所有key-value对构成的Set集合
    Set entrySet = map.entrySet();
    Iterator iterator1 = entrySet.iterator();
    //方式一:
    while (iterator1.hasNext()) {
        Object obj = iterator1.next();
        //entrySet集合中的元素都是entry
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "-->" + entry.getValue());
    }
    System.out.println("--------------");

    //方式二:
    Set keySet = map.keySet();
    Iterator iterator2 = keySet.iterator();
    while (iterator2.hasNext()) {
        Object key = iterator2.next();
        Object value = map.get(key);
        System.out.println(key + "==" + value);
    }
}

总结:常用方法:

  • 添加:put(Object key, Object value)
  • 删除:remove(Object key)
  • 修改:put(Object key, Object value)
  • 查询:get(Object key)
  • 长度:size()
  • 遍历:keySet() / values() / entrySet()

11.6.4 内存结构说明:(难点)

11.6.4.1 HashMap在JDK 7.0 中实现原理:

HashMap的存储结构:

JDK 7.0 以前的版本:HashMap是数组 + 链表结构(地址链表法)

JDK 8.0 版本以后:HashMap是数组+链表+红黑树实现

image-20220411083609902

对象创建和添加过程:

HashMap map = new HashMap()

在实例化以后,底层创建了长度是16的一维数组 Entry[] table

…可能已经执行过多次put…

map.put(key1,value1)

  • 首先,调用key1所在类的hashCode 计算key1的哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
  • 如果此位置上的数据为空,此时key1-value1添加成功。—情况1
  • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已存在的一个或多个数据的哈希值:
    • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。—情况2
    • 如果key1的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类equals(key2)方法,比较:
      • 如果equals()返回false:此时key1-value1添加成功。情况3
      • 如果equals()返回true:使用value1替换value2

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据赋值过来。

HashMap的扩容

当HashMap的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,原数组中的数据必须重新集散其在新数组中的位置,并放进去,这就是resize。

HashMap扩容时机

当HashMap中的元素个数超过数组大小(数组总大小 length,不是数组中个数)* loadFactor时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTORY)为0.75,这是一个折中取值。也就是说,在默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16 * 0.75 = 12(这个值就是代码中threshold值,也叫做临界值)的时候,就把数组的大小扩展为2 * 16 = 32,即扩大一倍,然后重新计算每个元素在数组中位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

11.6.4.2 HashMap在JDK 8.0 底层实现原理:

HashMap的存储结构:

HashMap的内部存储结构其实是数组+链表+红黑树的组合。

image-20220411083617469

HashMap添加元素的过程:

当实例化一个HashMap时,会初始化initialCapacity 和 loadFactory,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为"桶"(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子节点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

HashMap的扩容机制

  • 当HashMap中的其中一个链的对象个数没有达到8个和JDK 7.0 以前的扩容方式一样。

  • 当HashMap中的其中一个链的对象个数达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成Tree Node类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树在转为链表。

JDK 8.0 与 JDK 7.0 中HashMap底层的变化:

  1. new HashMap():底层没有创建一个长度为16的数组
  2. JDK 8.0 底层的数组时:Node[] ,而非 Entry[]
  3. 首次调用put() 方法时,底层创建长度为16的数组。
  4. JDK 7.0 底层结构只有:数组 + 链表。JDK 8.0 中底层结构:数组 + 链表 + 红黑树。
    • 形成链表时,七上八下(JDK7:新的元素指向旧的元素。JDK8:旧的元素指向新的元素)
    • 此数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度>64时,此时此索引位置上的所有数据改为红黑树存储。
11.6.4.3 HashMap底层典型属性的属性说明:
  • DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16
  • DEFAULT_LOAD-FACTORY:HashMap的默认加载因子:0.75
  • threshold:扩容的临界值,=容量*填充因子:16 * 0.75=>12
  • TREEIFY_THRESHOLD:Bucket中链表大于该默认值,转化为红黑树:JDK 8.0 引入
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化最小的hash表容量:64
11.6.4.4 LinkedHashMap的底层实现原理
  • LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap.
  • 区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node。
  • 与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序;迭代顺序与Key-value对的插入顺序一致。

HashMap中内部类Node源码:

static class Node<K,V> implements Map.Entry<K,V>{
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

LinkedHashMap中内部类Entry源码:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;//能够记录添加的元素的先后顺序
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

11.6.5 TreeMap的使用

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象 要照key进行排:自然排序、定制排序

代码示例:

//自然排序
@Test
public void test() {
    TreeMap map = new TreeMap();
    User u1 = new User("Tom", 23);
    User u2 = new User("Jarry", 18);
    User u3 = new User("Bruce", 56);
    User u4 = new User("Davie", 23);

    map.put(u1, 98);
    map.put(u2, 16);
    map.put(u3, 92);
    map.put(u4, 100);

    Set entrySet = map.entrySet();
    Iterator iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}

//定制排序:按照年龄大小排
@Test
public void test2() {
    TreeMap map = new TreeMap(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            if (o1 instanceof User && o2 instanceof User) {
                User u1 = (User) o1;
                User u2 = (User) o2;
                return Integer.compare(u1.getAge(), u2.getAge());
            }
            throw new RuntimeException("输入数据类型错误");
        }
    });
    User u1 = new User("Tom", 23);
    User u2 = new User("Jarry", 18);
    User u3 = new User("Bruce", 56);
    User u4 = new User("Davie", 23);

    map.put(u1, 98);
    map.put(u2, 16);
    map.put(u3, 92);
    map.put(u4, 100);

    Set entrySet = map.entrySet();
    Iterator iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "=" + entry.getValue());
    }
}

11.6.6 使用Properties读取配置文件

代码示例:

//Properties:常用来处理配置文件。key和value都是String类型
public static void main(String[] args)  {
    FileInputStream fis = null;
    try {
        Properties pros = new Properties();

        fis = new FileInputStream("jdbc.properties");
        pros.load(fis);//加载流对应的文件

        String name = pros.getProperty("name");
        String password = pros.getProperty("password");

        System.out.println("name = " + name + ", password = " + password);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

11.6.7 面试题

  1. HashMap的底层实现原理?
  2. HashMap和HashTable的异同?
  3. CurrentHashMap与Hashtable的的异同?
  4. 负载因子值的大小,对HashMap的影响?
    • 负载因子的大小决定了HashMap的数据密度。
    • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
    • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容,也会影响性能,建议初始化预设大一点的空间
    • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

11.7 Collection工具类的使用

11.7.1 作用:

Collections是一个操作Set、List和Map等集合的工具类

Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

11.7.2 常用方法:

11.7.2.1 排序操作
  • reverse(List):反转List中元素的顺序
  • shuffle(List):对List集合元素进行随机排序
  • sort(List):根据元素的顺序对指定List集合元素升序排序
  • sort(List,Comparator):根据指定的Comparator产生的顺序对List集合进行排序
  • swap(List ,int , int ):将指定list集合中的i处元素和j处元素进行交换

代码示例:

@Test
public void test1() {
    List list = new ArrayList();
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(-97);
    list.add(0);
    System.out.println(list);//[123, 43, 765, -97, 0]

    //reverse(List):反转 List 中元素的顺序
    Collections.reverse(list);
    System.out.println(list);//[0, -97, 765, 43, 123]

    //shuffle(List):对 List 集合元素进行随机排序
    Collections.shuffle(list);
    System.out.println(list);//[765, -97, 123, 0, 43]

    //sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
    Collections.sort(list);
    System.out.println(list);//[-97, 0, 43, 123, 765]

    //swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
    Collections.swap(list,1,4);
    System.out.println(list);//[-97, 765, 43, 123, 0]
}
11.7.2.2 查找、替换
  • Object max(Collection):根据元素的自然排序,返回给定集合中最大元素
  • Object max(Collection, Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
  • Object min(Collection)
  • Object min(Collection, Comparator)
  • int frequency(Collection , Object):返回指定集合中指定元素的出现次数
  • void copy(List dest, List src):将src中的内容复制到dest中
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值。

代码示例:

@Test
public void test2(){
    List list = new ArrayList();
    list.add(123);
    list.add(123);
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(-97);
    list.add(0);
    System.out.println(list);//[123, 43, 765, -97, 0]
    //Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
    Comparable max = Collections.max(list);
    System.out.println(max);//765

    //Object min(Collection)
    Comparable min = Collections.min(list);
    System.out.println(min);//-97

    //int frequency(Collection,Object):返回指定集合中指定元素的出现次数
    int frequency = Collections.frequency(list,123);
    System.out.println(frequency);//3

    //void copy(List dest,List src):将src中的内容复制到dest中
    List dest = Arrays.asList(new Object[list.size()]);
    System.out.println(dest.size());//7
    Collections.copy(dest,list);
    System.out.println(dest);//[123, 123, 123, 43, 765, -97, 0]
    //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
}
11.7.2.3 同步控制

Collections 类中提供了很多synchronizedXxx()方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

代码示例:

@Test
public void test3() {
    List list = new ArrayList();
    list.add(123);
    list.add(123);
    list.add(123);
    list.add(43);
    list.add(765);
    list.add(-97);
    list.add(0);
    System.out.println(list);//[123, 43, 765, -97, 0]
    //返回的list1即为线程安全的List
    List list1 = Collections.synchronizedList(list);
    System.out.println(list1);//[123, 123, 123, 43, 765, -97, 0]
}

11.8 数据结构简述

计划后续专门开一个系列来聊一下数据结构那些事

11.8.1 数据结构概述

数据结构(Data Structure是一门和计算机硬件与软件都密切相关的学科,它的研究重点是在计算的程序设计领域中探讨如何在计算机中组织和存储数据并进行高效率的运用,涉及的内容包含:数据的逻辑关系、数据的存储结构、排序算法(Algorithm)、查找(或搜索)等

11.8.2 数据结构与算法的理解:

程序能否快速而高效地完成预订的任务,取决于是否选对了数据结构,而程序是否能清楚而正确地把问题解决,则取决于算法。

所以大家认为:“Alorithms + Data Strutures = Programs”(出自:Pascal之父Nicklaus Wirth)

总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体。

11.8.3 数据结构研究对象

11.8.3.1 数据结构的研究对象

集合结构

image-20220411083628888

一对一:线性结构

image-20220411085350938

一对多:树形结构

image-20220411083641606

多对多:图形结构

image-20220411083649005

11.8.3.2 数据的存储结构:

线性表(顺序表、链表、栈、队列) 树 图

说明:习惯上顺序表和链表看做基本数据结构(或真实数据结构)习惯把栈、队列、树、图看做抽象数据类型,简称ADT

11.8.4 思维导图

思维导图下载地址:https://gitee.com/realbruce/blogImage.git

数据结构与算法:

image-20220411083658272

12. Java之泛型

12.1 泛型简介

12.1.1 泛型的概念

  • 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者某个方法的返回值及参数类型。这个类型参数将在(例如,继承或实现这个接口,用这个类型声明变量,创建对象时确定(即传入实际的类型参数,也称为类型实参))。
  • 从JDK5.0以后,Java引入了"参数化类型(Parameterized type)"的概念,允许我们在创建集合时再指定集合元素的类型,正如:List,这表明该List只能保存字符串类型的对象。
  • JDK 5.0 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参

12.1.2 泛型的引入背景

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK 1.5 之前只能把元素类型设计为Object,JDK 1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection,List,ArrayList这个就是类型参数,即泛型。

12.1.3 引入泛型的目的

  1. 解决元素存储的安全性问题,好比商品,药品标签,不会弄错。
  2. 解决获取数据元素时,需要类型强制转换问题,不用每回拿商品、药品都要辨别。

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮

12.2 泛型在集合中的应用

12.2.1 在集合中没有使用泛型的例子

@Test
public void test1(){
    ArrayList list = new ArrayList();
    //需求:存放学生的成绩
    list.add(78);
    list.add(76);
    list.add(89);
    list.add(88);
    //问题一:类型不安全
    //        list.add("Tom");

    for(Object score : list){
        //问题二:强转时,可能出现ClassCastException
        int stuScore = (Integer) score;

        System.out.println(stuScore);
    }
}

图示:

image-20220411083706017

12.2.2 集合中使用泛型的例子1

//在集合中使用泛型,以ArrayList为例
@Test
public void test1(){
    ArrayList<String> list = new ArrayList<>();
    list.add("AAA");
    list.add("BBB");
    list.add("FFF");
    list.add("EEE");
    list.add("CCC");
	//遍历方式一:
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
    System.out.println("-------------");
    //便利方式二:
    for (String str:
         list) {
        System.out.println(str);
    }
}

图示:

image-20220411083712855

12.2.3 在集合中使用泛型的例子2

@Test
//在集合中使用泛型的情况:以HashMap为例
public void test2(){
    Map<String,Integer> map = new HashMap<>();//jdk7新特性:类型推断
    map.put("Tom",26);
    map.put("Jarry",30);
    map.put("Bruce",28);
    map.put("Davie",60);
    //嵌套循环
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
    Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();

    while (iterator.hasNext()){
        Map.Entry<String, Integer> entry = iterator.next();
        String key = entry.getKey();
        Integer value = entry.getValue();
        System.out.println(key+"="+value);
    }
}

12.2.4 集合中使用泛型总结

  1. 集合接口或集合类在JDK 5. 0是都修改为带泛型的结构

  2. 在实例化集合类时,可以指明具体的泛型类型

  3. 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。

    比如:add(E e) —> 实例化以后:add(Integer e)

  4. 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换

  5. 如果实例化时,没有指明泛型的类型。默认类型为java.lang.Object类型。

12.3 自定义泛型结构

泛型类、泛型接口、泛型方法

12.3.1 泛型的声明

  • interface List< T >class GenTest< K , V > 其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。
  • 常用T表示,是Type的缩写。

12.3.2 泛型的实例化:

一定要在类名后面指定类型参数的值(类型)。如:

List< String > strList = new ArrayList < String >();

Iterator < Customer > iterator = customers.iterator();

  • T只能是类,不能用基本数据类型填充。但是可以使用包装类填充
  • 把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想
//JDK 5.0以前
Comparable c = new Date();
System.out.println(c.comparaTo("red");
                   
//JDK 5.0以后
Comparable <Date> c = new Date();
System.out.println(c.comparaTo("red");        

总结:使用泛型的主要优点在于能够在编译时而不是在运行时检测错误

image-20220411083720481

image-20220411083729399

分析两者区别

12.3.3 注意点

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如< E1, E2, E3 >

  2. 泛型类的构造器如下:public GenericClass(){}

    而下面的是错误的:public GenericClass< E >{}

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  4. 泛型不同的引用不能相互赋值

尽管在编译时ArrayList< String >和ArrayList< Integer >是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。

  1. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。

建议:泛型要使用一路都用。要不用,一路都不用

  1. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  2. JDK 7.0 泛型的简化操作: ArrayList< Fruit >first = new ArrayList<>();(类型推断)

  3. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。

  4. 在类/接口上声明的泛型,在本类或接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型

  5. 异常类不能是泛型的。

  6. 不能使用new E[]。但是可以:E[] elements = (E[]) new Object[capacity]

    image-20220411083739589

> 参考:ArrayList源码中声明:`Object[] elementData`,而非泛型参数类型数组。

  1. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    -   子类不保留父类的泛型:按需实现
        -   没有类型---擦除
        -   具体类型
    -   子类保留父类的泛型:泛型子类
        -   全部保留
        -   部分保留
    -   结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
    
    

    代码示例:

    class Father<T1, T2> {
    }
    
    /**
     * 定义泛型子类Son
     * 情况一:继承泛型父类后不保留父类的泛型
     */
    //1.没有指明类型  擦除
    class Son1<A, B> extends Father {//等价于class Son1 extends Father<Object,Odject>{}
    }
    
    //2.指定具体类型
    class Son2<A, B> extends Father<Integer, String> {
    }
    
    /**
     * 定义泛型子类Son
     * 情况二:继承泛型父类后保留泛型类型
     */
    //1.全部保留
    class Son3<T1, T2, A, B> extends Father<T1, T2> {
    }
    //2.部分保留
    class Son4<T2, A, B> extends Father<Integer,T2>{
    }
    
    

12.3.4 自定义泛型结构

12.3.4.1 自定义泛型类

代码示例:

/**
 * 自定义泛型类Order
 */
class Order<T> {
    private String orderName;
    private int orderId;
    //使用T类型定义变量
    private T orderT;

    public Order() {
    }
    //使用T类型定义构造器
    public Order(String orderName, int orderId, T orderT) {
        this.orderName = orderName;
        this.orderId = orderId;
        this.orderT = orderT;
    }

    //这个不是泛型方法
    public T getOrderT() {
        return orderT;
    }
    //这个不是泛型方法
    public void setOrderT(T orderT) {
        this.orderT = orderT;
    }
    //这个不是泛型方法
    @Override
    public String toString() {
        return "Order{" +
                "orderName='" + orderName + '\'' +
                ", orderId=" + orderId +
                ", orderT=" + orderT +
                '}';
    }
//    //静态方法中不能使用类的泛型。(区分类的泛型和泛型方法)
//    public static void show(T orderT){
//        System.out.println(orderT);
//    }

//    //try-catch中不能是泛型的。
//    public void show(){
//        try {
//
//        }catch (T t){
//
//        }
//    }

    //泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。
    //换句话说,泛型方法所属的类是不是泛型类都没有关系。
    //泛型方法,可以声明为静态的。
    // 原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
    public static <E> List<E> copyFromArryToList(E[] arr) {
        ArrayList<E> list = new ArrayList<>();
        for (E e :
                list) {
            list.add(e);
        }
        return list;
    }
}

自定义泛型类Order的使用

@Test
public void test1() {
    //如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为Object类型
    //要求:如果大家定义了类是带泛型的,建议在实例化时要指明类的泛型。
    Order order = new Order();
    order.setOrderT(123);
    System.out.println(order.getOrderT());

    order.setOrderT("abc");
    System.out.println(order.getOrderT());

    //建议:实例化时指明类的泛型
    Order<String> order1 = new Order<>("Tom", 16, "male");
    order1.setOrderT("AA:BBB");
    System.out.println(order1.getOrderT());
}

@Test
//调用泛型方法
public void test2(){
    Order<String> order = new Order<>();
    Integer [] arr = new Integer[]{1,2,3,4,5,6};

    List<Integer> list = order.copyFromArryToList(arr);
    System.out.println(list);
}


12.3.4.2 自定义泛型接口

代码示例:

/**
 * 自定义泛型接口
 */
public interface DemoInterface <T> {
    void show();
    int size();
}
//实现泛型接口
public class Demo implements DemoInterface {
    @Override
    public void show() {
        System.out.println("hello");
    }
    @Override
    public int size() {
        return 0;
    }
}
@Test
//测试泛型接口
public void test3(){
    Demo demo = new Demo();
    demo.show();
}
12.3.4.3 自定义泛型方法
  • 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
  • 泛型方法的格式:[访问权限] < 泛型> 返回类型 方法名(泛型标识 参数名称) 抛出的异常
  • 泛型方法声明泛型时也可以指定上限

代码示例:

//泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。
//换句话说,泛型方法所属的类是不是泛型类都没有关系。
//泛型方法,可以声明为静态的。
// 原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
public static <E> List<E> copyFromArryToList(E[] arr) {
    ArrayList<E> list = new ArrayList<>();
    for (E e :
         list) {
        list.add(e);
    }
    return list;
}
12.3.4.4 总结:
  • 泛型实际上就是标签,声明时不知道类型,在使用时指明
  • 定义泛型结构,即:泛型类、接口、方法、构造器时贴上泛型的标签
  • 用泛型定义类或接口是放到类名或接口名后面,定义泛型方法时在方法名前加上

12.3.5 泛型的应用场景

public class DAO<T> {//表的共性操作的DAO

    //添加一条记录
    public void add(T t){

    }

    //删除一条记录
    public boolean remove(int index){

        return false;
    }

    //修改一条记录
    public void update(int index,T t){

    }

    //查询一条记录
    public T getIndex(int index){

        return null;
    }

    //查询多条记录
    public List<T> getForList(int index){

        return null;
    }
    //泛型方法
    //举例:获取表中一共有多少条记录?获取最大的员工入职时间?
    public <E> E getValue(){
        return null;
    }
}

CustomerDao.java:

public class CustomerDAO extends DAO<Customer>{//只能操作某一个表的DAO
}

StudentDao.java:

public class StudentDAO extends DAO<Student> {//只能操作某一个表的DAO
}

12.4 泛型在继承上的体现

泛型在继承方面的体现:

虽然类A是类B的父类,但是G< A>G< B>二者不具备子父类关系,二者是并列关系。

image-20220411083756955

补充:类A是类B的父类,A< G>B< G>的父类

代码示例:

@Test
public void test1(){

    Object obj = null;
    String str = null;
    obj = str;

    Object[] arr1 = null;
    String[] arr2 = null;
    arr1 = arr2;
    //编译不通过
    //        Date date = new Date();
    //        str = date;
    List<Object> list1 = null;
    List<String> list2 = new ArrayList<String>();
    //此时的list1和list2的类型不具子父类关系
    //编译不通过
    //        list1 = list2;
    /*
        反证法:
        假设list1 = list2;
           list1.add(123);导致混入非String的数据。出错。

         */

    show(list1);
    show1(list2);
}

public void show1(List<String> list){

}

public void show(List<Object> list){

}

@Test
public void test2(){

    AbstractList<String> list1 = null;
    List<String> list2 = null;
    ArrayList<String> list3 = null;

    list1 = list3;
    list2 = list3;

    List<String> list4 = new ArrayList<>();
}

12.5 通配符

12.5.1 通配符的使用(多态的需求)

  • 使用类型通配符:

    比如:List< ? >,Map< ?,? >

    List< ?>List< String >List< Object >等各种泛型List的父类。

  • 读取List< ?> 的对象list中元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object

  • 写入list中的元素时,不可以。因为我们不知道C的元素类型,我们不能像其中添加对象。除了null之外。

说明:

  • 将任意元素加入到其中不是类型安全的

    Collection<?> c = new ArrayList< String > ()

    c.add(new Object());/ /编译时错误

    因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有参数类型E作为集合的元素类型。我们传给add的任何参数都必须是一个已知的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。

  • 唯一的例外的是null,它是所有类型的成员。

  • 我们可以调用get()方法并使用其返回值,返回值是一个未知的类型,但是我们知道,它总是一个Object。

代码示例:

@Test
public void test3(){
    List<Object> list1 = null;
    List<String> list2 = null;

    List<?> list = null;

    list = list1;
    list = list2;
    //编译通过
    //        print(list1);
    //        print(list2);

    //
    List<String> list3 = new ArrayList<>();
    list3.add("AA");
    list3.add("BB");
    list3.add("CC");
    list = list3;
    //添加(写入):对于List<?>就不能向其内部添加数据。
    //除了添加null之外。
    //        list.add("DD");
    //        list.add('?');

    list.add(null);

    //获取(读取):允许读取数据,读取的数据类型为Object。
    Object o = list.get(0);
    System.out.println(o);
}

public void print(List<?> list){
    Iterator<?> iterator = list.iterator();
    while(iterator.hasNext()){
        Object obj = iterator.next();
        System.out.println(obj);
    }
}

12.5.2 注意点

//注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
    
}

//注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
    
}

//注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<> list2 new ArrayList<?>();

12.5.3 有限制的通配符

  • < ? >:允许所有泛型的引用调用

  • 通配符指定上限

    上限 extends :使用时指定的类型必须是继承某个类,或者是实现某个接口,即 <=

  • 通配符指定下限

    下限 super :使用时指定的类型不能小于操作的类,即 >=

  • 举例:

    • <? extends Number> (无穷小,Number \ ]

      只允许泛型为Number及Number子类的引用调用

    • < ? super Number> / [Number,无穷大 )

      只允许泛型为Number即Number的父类的引用调用

    • < ? extends Comparable >

      只允许泛型为实现Comparable接口的实现类的引用调用

代码示例:

@Test
public void test4(){

    List<? extends Person> list1 = null;
    List<? super Person> list2 = null;

    List<Student> list3 = new ArrayList<Student>();
    List<Person> list4 = new ArrayList<Person>();
    List<Object> list5 = new ArrayList<Object>();

    list1 = list3;
    list1 = list4;
    //        list1 = list5;

    //        list2 = list3;
    list2 = list4;
    list2 = list5;

    //读取数据:
    list1 = list3;
    Person p = list1.get(0);
    //编译不通过
    //Student s = list1.get(0);

    list2 = list4;
    Object obj = list2.get(0);
    编译不通过
    //        Person obj = list2.get(0);

    //写入数据:
    //编译不通过
    //        list1.add(new Student());

    //编译通过
    list2.add(new Person());
    list2.add(new Student());
}

13. Java之IO流

13.1 FIle类的使用

13.1.1 File类的理解

  • File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)。
  • File类声明在 java.io 包下:文件和文件路径的抽象表示形式,与平台无关。
  • File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
  • 后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。

13.1.2 File的实例化

13.1.2.1 常用的构造器
  • File(String filePath)
  • File(String ParentPath, String childPath)
  • File(File parentFile, String childPath)

代码示例:

@Test
public void test1() {
    //构造器1
    File file1 = new File("hello.txt");
    File file2 = new File("E:\\workspace_idea\\JavaSenic\\IO\\hello.txt");
    System.out.println(file1);
    System.out.println(file2);

    //构造器2
    File file3 = new File("E:\\workspace_idea\\JavaSenior", "hello.txt");
    System.out.println(file3);

    //构造器3
    File file4 = new File(file3, "hi.txt");
    System.out.println(file4);
}

image-20220411083920416

13.1.2.2 路径分类
  • 相对路径:相较于某个路径,指明的路径。
  • 绝对路径:包含盘符在内的文件或文件目录的路径

说明:

  • IDEA中:

    • 如果使用JUnit中的单元测试方法测试,相对路径即为当前Module下。
    • 如果使用main()测试,相对路径即为当前的Project下
  • Eclipse中:

    • 不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。
13.1.2.3 路径分隔符
  • windows和DOS系统默认 \ 来表示

  • UNIX 和 URL使用 / 来表示

  • Java程序支持跨平台运行,因此路径分隔符要慎用。

  • 为了解决这个隐患,File类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符。

    举例:

    //windows和DOS系统
    File file1 = new File("E:\\io\\test.txt");
    //UNIX和URL
    File file = new File("E:/io/test.txt");
    //java提供的常量
    File file = new File("E:"+File.separator+"io"+File.separator+"test.txt");
    

13.1.3 File类的常用方法

13.1.3.1 File类的获取功能
  • public String getAbsolutePath():获取绝对路径
  • public String getPath():获取路径
  • public String getName():获取名称
  • public String getParent():获取上层文件目录路径。若无,返回null
  • public long length():获取文件长度(即:字节数)。不能获取目录的长度。
  • public long lastModified():获取最后一次的修改时间,毫秒值
  • 如下的两个方法适用于文件目录:
  • public String[] list():获取指定目录下的所有文件或文件目录的名称数组
  • public File[] listFiles():获取指定目录下的所有文件或者文件目录的File数组。

代码示例:

@Test
public void test2(){
    File file1 = new File("hello.txt");
    File file2 = new File("d:\\io\\hi.txt");

    System.out.println(file1.getAbsolutePath());
    System.out.println(file1.getPath());
    System.out.println(file1.getName());
    System.out.println(file1.getParent());
    System.out.println(file1.length());
    System.out.println(new Date(file1.lastModified()));

    System.out.println();

    System.out.println(file2.getAbsolutePath());
    System.out.println(file2.getPath());
    System.out.println(file2.getName());
    System.out.println(file2.getParent());
    System.out.println(file2.length());
    System.out.println(file2.lastModified());
}
@Test
public void test3(){
    File file = new File("D:\\workspace_idea1\\JavaSenior");

    String[] list = file.list();
    for(String s : list){
        System.out.println(s);
    }
    System.out.println();

    File[] files = file.listFiles();
    for(File f : files){
        System.out.println(f);
    }
}
13.1.3.2 File类的重命名功能
  • public boolean renameTo(File dest):把文件重命名为指定的文件路径
  • 注意:file1.renameTo(file2)为例:要想保证返回 true ,需要file1在硬盘中是存在的,且file2不能在硬盘中存在。

代码示例:

@Test
public void test4(){
    File file1 = new File("hello.txt");
    File file2 = new File("hi.txt");

    boolean renameTo = file1.renameTo(file2);
    System.out.println(renameTo);
}
13.1.3.3 File类的判断功能
  • public boolean isDirectory():判断是否是文件目录
  • public boolean isFile():判断是否是文件
  • public boolean exists():判断是否存在
  • public boolean canRead():判断是否可读
  • public boolean canWrite():判断是否可写
  • public boolean isHidden():判断是否隐藏

代码示例:

@Test
public void test5(){
    File file1 = new File("hello.txt");
    file1 = new File("hello1.txt");

    System.out.println(file1.isDirectory());
    System.out.println(file1.isFile());
    System.out.println(file1.exists());
    System.out.println(file1.canRead());
    System.out.println(file1.canWrite());
    System.out.println(file1.isHidden());

    System.out.println();

    File file2 = new File("d:\\io");
    file2 = new File("d:\\io1");
    System.out.println(file2.isDirectory());
    System.out.println(file2.isFile());
    System.out.println(file2.exists());
    System.out.println(file2.canRead());
    System.out.println(file2.canWrite());
    System.out.println(file2.isHidden());
}
13.1.3.4 File类的创建功能
  • 创建硬盘中对应的文件或文件目录
  • public boolean creatNewFile():创建文件。如果文件存在,则不创建,返回false
  • public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs():创建文件目录。如果此文件目录存在,就不创建了。如果上层文件目录不存在,一并创建。

代码示例:

@Test
public void test6() throws IOException {
    File file1 = new File("hi.txt");
    if(!file1.exists()){
        //文件的创建
        file1.createNewFile();
        System.out.println("创建成功");
    }else{//文件存在
        file1.delete();
        System.out.println("删除成功");
    }
}
@Test
public void test7(){
    //文件目录的创建
    File file1 = new File("d:\\io\\io1\\io3");

    boolean mkdir = file1.mkdir();
    if(mkdir){
        System.out.println("创建成功1");
    }

    File file2 = new File("d:\\io\\io1\\io4");

    boolean mkdir1 = file2.mkdirs();
    if(mkdir1){
        System.out.println("创建成功2");
    }
    //要想删除成功,io4文件目录下不能有子目录或文件
    File file3 = new File("D:\\io\\io1\\io4");
    file3 = new File("D:\\io\\io1");
    System.out.println(file3.delete());
}
13.1.3.5 File类的删除功能
  • 删除磁盘中的文件或文件目录
  • public boolean delete():删除文件或者文件夹
  • 删除注意事项:Java中删除不走回收站。

13.1.4 内存解析

image-20220411083933173

13.1.5 小练习

利用 FIle 构造器,new 一个文件目录file

1)在其中创建多个文件和目录

2)编写方法,实现删除file中指定文件的操作

@Test
public void test1() throws IOException {
    File file = new File("E:\\io\\io1\\hello.txt");
    //创建一个与file同目录下的另外一个文件,文件名为:haha.txt
    File destFile = new File(file.getParent(),"haha.txt");
    boolean newFile = destFile.createNewFile();
    if(newFile){
        System.out.println("创建成功!");
    }
}

判断指定目录下是否有后缀名为 .jpg 的文件,如果有,就输出该文件名称

public class FindJPGFileTest {

    @Test
    public void test1(){
        File srcFile = new File("d:\\code");

        String[] fileNames = srcFile.list();
        for(String fileName : fileNames){
            if(fileName.endsWith(".jpg")){
                System.out.println(fileName);
            }
        }
    }
    @Test
    public void test2(){
        File srcFile = new File("d:\\code");

        File[] listFiles = srcFile.listFiles();
        for(File file : listFiles){
            if(file.getName().endsWith(".jpg")){
                System.out.println(file.getAbsolutePath());
            }
        }
    }
    /*
	 * File类提供了两个文件过滤器方法
	 * public String[] list(FilenameFilter filter)
	 * public File[] listFiles(FileFilter filter)

	 */
    @Test
    public void test3(){
        File srcFile = new File("d:\\code");

        File[] subFiles = srcFile.listFiles(new FilenameFilter() {

            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".jpg");
            }
        });

        for(File file : subFiles){
            System.out.println(file.getAbsolutePath());
        }
    }
}

遍历指定目录所有文件名称,包括子文件目录中的文件。

拓展1:并计算目录占用空间的大小

拓展2:删除指定文件目录及其下的所有文件

13.1.5.1 递归遍历
public class ListFileTest {

    public static void main(String[] args) {
        // 递归:文件目录
        /** 打印出指定目录所有文件名称,包括子文件目录中的文件 */

        //1.创建目录对象
        File file = new File("E:\\test");

        //2.打印子目录
        printSubFile(file);

    }

    /**
     * 递归方法遍历所有目录下的文件
     *
     * @param dir
     */
    public static void printSubFile(File dir) {
        //打印子目录
        File[] files = dir.listFiles();
        for (File f : files) {
            if (f.isDirectory()) {//如果为文件目录,则递归调用自身
                printSubFile(f);
            } else {
                System.out.println(f.getAbsolutePath());//输出绝对路径
            }
        }
    }  
13.1.5.2 递归计算文件夹大小
 // 拓展1:求指定目录所在空间的大小
    // 求任意一个目录的总大小
    public long getDirectorySize(File file) {
        // file是文件,那么直接返回file.length()
        // file是目录,把它的下一级的所有大小加起来就是它的总大小
        long size = 0;
        if (file.isFile()) {
            size += file.length();
        } else {
            File[] allFiles = file.listFiles();// 获取file的下一级
            // 累加all[i]的大小
            for (File f : allFiles) {
                size += getDirectorySize(f);//f的大小
            }
        }
        return size;
    }
13.1.5.3 递归删除文件夹
/**
     * 拓展2:删除指定的目录
     */
    public void deleteDirectory(File file) {
        // 如果file是文件,直接delete
        // 如果file是目录,先把它的下一级干掉,然后删除自己
        if (file.isDirectory()) {
            File[] allFiles = file.listFiles();
            //递归调用删除file下一级
            for (File f : allFiles) {
                deleteDirectory(f);
            }
        } else {
            //删除文件
            file.delete();
        }
    }
}

13.2 IO流概述

13.2.1 简述

  • IO是Input/OutPut的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

  • Java程序中,对于数据的输入输出操作以"流(Stream)"的方式进行。

  • Java.IO包下提供了各种"流"类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

13.2.2 流的分类

操作数据单位:字节流、字符流

  • 对于文本文件(.txt、.java、.c、.cpp),使用字符流处理
  • 对于非文本文件(.jpg、.mp3、.mp4、.avi、.doc、.ppt、…),使用字节流处理

数据的流向:输入流、输出流

  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

流的角色:节点流、处理流

节点流:直接从数据源或目的地读写数据。

image-20220411083946646

处理流:不直接连到数据源或目的地,而是"连接"在已存在的流(字节流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

image-20220411083955493

图示

image-20220411084004446

13.2.3 IO流的体系分类

13.2.3.1 总体分类

image-20220411084014537

红框为抽象基类,篮框为常用IO流

13.2.3.2 常用的几个IO流结构
抽象基类节点流缓冲流(处理流的一种)
InputStreamFileInputStream(read(byte[] buffer))BufferedInputStream(read(byte[] buffer))
OutputStreamFileOutputStream(write(byte[] buffer,0,len))BufferedOutputStream(write(byte[] buffer,0,len)) / flush()
ReaderFileReader(read(char[] cbuf))BufferedReader(read(char[] cbuf) / readLine())
WriterFileWriter (write(char[] cbuf,0,len)BufferedWriter (write(char[] cbuf,0,len) / flush()
13.2.3.3 对抽象基类的说明:
抽象基类字节流字符流
输入流InputSteamReader
输出流OutputSteamWriter
  • 说明:Java的lO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。
  • 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

3.3.1InputSteam & Reader

  • InputStream和Reader是所有输入流的基类。
  • InputStream(典型实现:FileInputStream)
    • int read()
    • int read(byte[] b)
    • int read(byte[] b,int off,int len)
  • Reader(典型实现:FileReader)
    • int read()
    • int read(char[] c)
    • int read(char[] c,int off,int len)
  • 程序中打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。
  • FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader。

InputSteam:

  • int read()

    从输入流中读取数据的下一个字节。返回0到255范围内的int字节值。如果因为已经到达流末尾而没有可用的字节,则返回值-1。

  • int read(byte[] b)

    从此输入流中将最多b.length个字节的数据读入一个byte数组中。如果因为已经到达流末尾而没有可用的字节,则返回值-1.否则以整数形式返回实际读取的字节数。

  • int read(byte[] b,int off,int len)

    将输入流中最多len个数据字节读入byte数组。尝试读取len个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值-1。

  • public void close throws IOException

    关闭此输入流并释放与该流关联的所有系统资源。

Reader:

  • int read()

    读取单个字符。作为整数读取的字符,范围在0到65535之间(0x0000-0xffff)(2个字节的Unicode码),如果已经达到流的末尾,则返回-1.

  • int read(char[] cbuf, int off, int len)

    将字符读入数组的某一部分。存到数组cbuf中,从off出开始存储,最多读len字符。如果已经达到流的末尾,则返回-1。否则返回本次读取的字符数。

  • public void close throws IOEXception

    关闭此输入流并释放与该流关联的所有系统资源

3.3.2 OutputSteam & Writer

  • OutputStream 和 Writer也非常相似:

    • void write(int b/int c);
    • void write(byte[] b/char[] cbuf);
    • void write(byte[] b/char[] buff,int off,int len);
    • void flush();
    • void close(); //需要先刷新,在关闭此流
  • 因为字符流直接以字符作为操作单位,所以Writer可以用字符串来替换字符数组,即以String对象作为参数

    • void write(String str);
    • void write(String str, int off, int len);
  • FileOutputStream从文件系统中的某个文件中获得输出字节。FileOutPutStream用于写出非文本数据之类的原始字节流。要写出字符流,需要使用FileWriter

OutputStream:

  • void write(int b)

    将指定的字节写入此输出流。write的常规协定是:向输出流写入一个字节。要写入的字节是参数b的八个低位。b的24个高位将被忽略。即写入0-255范围的

  • void write(byte[] b)

    b.length个字节从指定的byte数组写入此输出流。write(b)的常规协定是:应该与调用write(b, 0 , b.length) 的效果完全相同。

  • void write(byte[], int off, int len)

    将指定byte数组中从偏移量off开始的len个字节写入此输出流。

  • public void flush() throws IOException

    刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入他们预期的目标。

  • public void close throws IOException

    关闭此输出流并释放与该流关联的所有系统资源。

Writer:

  • void write(int c)

    写入单个字符。要写入的字符包含在给定整数值的16个低位中,16高位被忽略。即写入0到65535之间的Unicode码。

  • void write(char[] cbuf)

    写入字符数组

  • void write(char[] cbuf, int off , int len)

    写入字符数组的某一部分。从off开始,写入len个字符

  • void write(String str)

    写入字符串。

  • void write(String str, int off, int len)

    写入字符串的某一部分

  • void fulsh()

    刷新该流的缓冲,即立即将他们写入预期目标。

  • public void close throws IOEception

    关闭此输出流并释放该流关联的所有系统资源

13.2.4 输入、输出标准化过程

13.2.4.1 输入过程:
  1. 创建File类的对象文件,指明读取的数据的来源。(要求此文件一定要存在)
  2. 创建相应的输入流,将File类的对象作为参数,传入流的构造器中。
  3. 具体的读入过程:创建相应的byte[] 或char[] 。
  4. 关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理。

13.2.4.2 输出过程:
  1. 创建File类的对象,指明写出的数据的位置。(不要求此文件一定要存在)
  2. 创建相应的输出流,将File类的对象作为参数,传入流的构造器中。
  3. 具体的写出过程:write(char[] / byte[] buffer,0 , len)
  4. 关闭流资源

说明:程序中出现的异常需要使用 try-catch-finally 处理

13.3 节点流(文件流)

13.3.1 文件字符流FileReader和FileWriter的使用

13.3.1.1 文件的输入

从文件中读取到内存(程序)中

步骤:

  1. 建立一个流对象,将已存在的一个文件加载进流FileReader fr = new FileReader(new File("Test.txt"));
  2. 创建一个临时存放数据的数组。fr.read(ch);
  3. 调用流对象的读取方法将流中的数据读入到数组中。fr.read(ch);
  4. 关闭资源。fr.close();

代码示例:

@Test
public void testFileReader1()  {
    FileReader fr = null;
    try {
        //1.File类的实例化
        File file = new File("hello.txt");

        //2.FileReader流的实例化
        fr = new FileReader(file);

        //3.读入的操作
        //read(char[] cbuf):返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
        char[] cbuf = new char[5];
        int len;
        while((len = fr.read(cbuf)) != -1){
            String str = new String(cbuf,0,len);
            System.out.print(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fr != null){
            //4.资源的关闭
            try {
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

注意点:

  1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
  2. 异常的处理:为了保证流的资源一定可以执行关闭操作,需要使用try-catch-finally处理
  3. 读入的文件一定要存在,否则就会报FileNotFoundException。
13.3.1.2 文件的输出

从内存(程序)到硬盘文件中

步骤:

  1. 创建流对象,建立数据存放文件Filewriter fw = new FileWriter(new File("Test.txt"))
  2. 调用流对象的写入方法啊,将数据写入流fw.write("HelloWord")
  3. 关闭流资源,并将流中的数据清空到文件中。fw.close()

代码示例:

@Test
public void testFileWriter() {
    FileWriter fw = null;
    try {
        //1.提供File类的对象,指明写出到的文件
        File file = new File("hello1.txt");

        //2.提供FileWriter的对象,用于数据的写出
        fw = new FileWriter(file,false);//true在原有文件上append添加

        //3.写出的操作
        fw.write("I have a dream!\n");
        fw.write("you need to have a dream!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.流资源的关闭
        if(fw != null){

            try {
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
13.3.1.3 小练习

实现文本文件的复制操作

@Test
public void testFileReaderFileWriter() {
    FileReader fr = null;
    FileWriter fw = null;
    try {
        //1.创建File类的对象,指明读入和写出的文件
        File srcFile = new File("hello.txt");
        File destFile = new File("hello2.txt");

        //不能使用字符流来处理图片等字节数据
        //            File srcFile = new File("test.jpg");
        //            File destFile = new File("test1.jpg");

        //2.创建输入流和输出流的对象
        fr = new FileReader(srcFile);
        fw = new FileWriter(destFile);

        //3.数据的读入和写出操作
        char[] cbuf = new char[5];
        int len;//记录每次读入到cbuf数组中的字符的个数
        while((len = fr.read(cbuf)) != -1){
            //每次写出len个字符
            fw.write(cbuf,0,len);

        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流资源
        try {
            if(fw != null)
                fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if(fr != null)
                fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

13.3.2 文件字节流FileInputStream 和FileOutputStream的使用

文件字节流与字符流操作类似,只是实例化对象操作和数据类型不同。

代码示例:

//使用字节流FileInputStream处理文本文件,可能出现乱码。
@Test
public void testFileInputStream() {
    FileInputStream fis = null;
    try {
        //1. 造文件
        File file = new File("hello.txt");

        //2.造流
        fis = new FileInputStream(file);

        //3.读数据
        byte[] buffer = new byte[5];
        int len;//记录每次读取的字节的个数
        while((len = fis.read(buffer)) != -1){

            String str = new String(buffer,0,len);
            System.out.print(str);

        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fis != null){
            //4.关闭资源
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

小练习

实现图片复制操作

@Test
public void testFileInputOutputStream()  {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //1.创建File对象
        File srcFile = new File("test.jpg");
        File destFile = new File("test2.jpg");

        //2.创建操流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);

        //3.复制的过程
        byte[] buffer = new byte[5];
        int len;
        while((len = fis.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流
        if(fos != null){
            //
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.3.3 注意点

  • 定义路径时,可以用 / 或 \ \。
  • 输出操作,对应的File可以不存在的。并不会报异常。
  • File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
  • File对应的磁盘中的文件如果存在:
    • 如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖
    • 如果流使用的构造器是:FileWriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容。
  • 读取文件时,必须保证文件存在,否则会报异常。
  • 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理
  • 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,…),使用字节流处理

13.4 缓冲流

13.4.1 缓冲流涉及到的类

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter

13.4.2 引入目的:

  • 作用:提高流的读取、写入的速度

  • 提高读写速度的原因:内部提供了一个缓冲区,默认情况下是8kb

    15211455493](C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220315211455493.png)

处理流与字节流的对比图示

image-20220411084030455

image-20220411084040272

13.4.3 使用说明

  • 当读取数据时,数据块读入缓冲区,其后的读操作则直接访问缓冲区。
  • 当使用BufferedInputStream读取字节文件时,BufferedInputStream 会一次性从文件中读取8192个(8kb),存在缓冲区,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。
  • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush(),可以强制将缓冲区的内容全部写入到输出流
  • flush()方法的使用:手动将buffer中内容写入到文件。
  • 如果是带缓冲区的流对象close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。

代码示例:

13.4.3.1 使用BufferedInputStream和BufferedOutputStream实现非文本的复制
@Test
public void testBufferedStream(){
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
    try {
        //1.造文件
        File srcFile = new File("test.jpg");
        File destFile = new File("test4.jpg");
        //2.造流
        //2.1造节点流
        FileInputStream fis = new FileInputStream(srcFile);
        FileOutputStream fos = new FileOutputStream(destFile);
        //2.2造缓冲流,可以合并书写
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);

        //3.文件读取、写出操作
        byte[] buffer = new byte[1024];
        int len;
        while ((len = bis.read(buffer)) != -1){
            bos.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流
        if (bos != null){
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bis != null){
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
13.4.3.2 使用BufferedReader和BufferedWriter实现文本文件的复制
@Test
public void testBufferedReaderBufferedWriter(){
    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
        //创建文件和相应的流
        br = new BufferedReader(new FileReader(new File("dbcp.txt")));
        bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));

        //读写操作
        //方式一:使用char[]数组
        //            char[] cbuf = new char[1024];
        //            int len;
        //            while((len = br.read(cbuf)) != -1){
        //                bw.write(cbuf,0,len);
        //    //            bw.flush();
        //            }

        //方式二:使用String
        String data;
        while((data = br.readLine()) != null){
            //方法一:
            //                bw.write(data + "\n");//data中不包含换行符
            //方法二:
            bw.write(data);//data中不包含换行符
            bw.newLine();//提供换行的操作

        }


    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关闭资源
        if(bw != null){

            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(br != null){
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.4.4 小练习

13.4.4.1 测试缓冲流和节点流文件复制速度

节点流实现复制方法

//指定路径下文件的复制
public void copyFile(String srcPath,String destPath){
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //1.造文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);

        //2.造流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);

        //3.复制的过程
        byte[] buffer = new byte[1024];
        int len;
        while((len = fis.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(fos != null){
            //4.关闭流
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fis != null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

缓冲流实现复制操作

//实现文件复制的方法
public void copyFileWithBuffered(String srcPath,String destPath){
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;

    try {
        //1.造文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        //2.造流
        //2.1 造节点流
        FileInputStream fis = new FileInputStream((srcFile));
        FileOutputStream fos = new FileOutputStream(destFile);
        //2.2 造缓冲流
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);

        //3.复制的细节:读取、写入
        byte[] buffer = new byte[1024];
        int len;
        while((len = bis.read(buffer)) != -1){
            bos.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源关闭
        //要求:先关闭外层的流,再关闭内层的流
        if(bos != null){
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        if(bis != null){
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}
13.4.4.2 实现图片加密操作

加密操作

  • 将图片文件通过字节流读取到程序中
  • 将图片的字节流逐一进行 ^ 操作
  • 将处理后的图片字节流输出
//图片的加密
@Test
public void test1() {

    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        fis = new FileInputStream("test.jpg");
        fos = new FileOutputStream("testSecret.jpg");

        byte[] buffer = new byte[20];
        int len;
        while ((len = fis.read(buffer)) != -1) {

            for (int i = 0; i < len; i++) {
                buffer[i] = (byte) (buffer[i] ^ 5);
            }

            fos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

解密操作

  • 将加密图片文件通过字节流读取到程序中
  • 将图片的字节流逐一进行 ^ 操作(原理:ABB = A)
  • 将处理后的图片字节流输出
//图片的解密
@Test
public void test2() {

    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        fis = new FileInputStream("testSecret.jpg");
        fos = new FileOutputStream("test4.jpg");

        byte[] buffer = new byte[20];
        int len;
        while ((len = fis.read(buffer)) != -1) {
          
            for (int i = 0; i < len; i++) {
                buffer[i] = (byte) (buffer[i] ^ 5);
            }

            fos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}
13.4.4.3 统计文本字符出现次数

实现思路:

  1. 遍历文本每一个字符
  2. 字符出现的次数存在Map中
  3. 把map中的数据写入文件
@Test
public void testWordCount() {
    FileReader fr = null;
    BufferedWriter bw = null;
    try {
        //1.创建Map集合
        Map<Character, Integer> map = new HashMap<Character, Integer>();

        //2.遍历每一个字符,每一个字符出现的次数放到map中
        fr = new FileReader("dbcp.txt");
        int c = 0;
        while ((c = fr.read()) != -1) {
            //int 还原 char
            char ch = (char) c;
            // 判断char是否在map中第一次出现
            if (map.get(ch) == null) {
                map.put(ch, 1);
            } else {
                map.put(ch, map.get(ch) + 1);
            }
        }

        //3.把map中数据存在文件count.txt
        //3.1 创建Writer
        bw = new BufferedWriter(new FileWriter("wordcount.txt"));

        //3.2 遍历map,再写入数据
        Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
        for (Map.Entry<Character, Integer> entry : entrySet) {
            switch (entry.getKey()) {
                case ' ':
                    bw.write("空格=" + entry.getValue());
                    break;
                case '\t'://\t表示tab 键字符
                    bw.write("tab键=" + entry.getValue());
                    break;
                case '\r'://
                    bw.write("回车=" + entry.getValue());
                    break;
                case '\n'://
                    bw.write("换行=" + entry.getValue());
                    break;
                default:
                    bw.write(entry.getKey() + "=" + entry.getValue());
                    break;
            }
            bw.newLine();
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关流
        if (fr != null) {
            try {
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bw != null) {
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.5 转换流

13.5.1 简介

  • 转换流提供了在字节流和字符流之间的转换
  • Java API提供了两个转换流
    • InputStreamReader:将InputStream转换为Reader
    • OutputStreamWriter:将Writer转换为OutputStream
  • 字节流中的数据都是字符时,转成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。
13.5.1.1 InputStreamReader

InputStreamReader 将一个字节的输入流转换为字符的输入流

解码:字节、字节数组 —>字符数组、字符串

构造器:

  • public InputStreamReader(InputStream in)
  • public InpurStreamReader(InputStream in, String charsetName)//可以指定编码集
13.5.1.2 OutputStreamWriter

OutputStreamWriter将一个字符的输出流转换为字节的输出流

编码:字符数组、字符串—>字节、字节数组

构造器:

  • public OutputStreamWriter(OutputStream out)
  • public OutputStreamWriter(OutputStream out, String charsetName)//可以指定编码集

图示:

image-20220411084053586

13.5.2 代码示例:

/**
综合使用InputStreamReader和OutputStreamWriter
     */
@Test
public void test1() {
    InputStreamReader isr = null;
    OutputStreamWriter osw = null;
    try {
        //1.造文件、造流
        File file1 = new File("dbcp.txt");
        File file2 = new File("dbcp_gbk.txt");

        FileInputStream fis = new FileInputStream(file1);
        FileOutputStream fos = new FileOutputStream(file2);

        isr = new InputStreamReader(fis, "utf-8");
        osw = new OutputStreamWriter(fos, "gbk");

        //2.读写过程
        char[] cbuf = new char[20];
        int len;
        while ((len = isr.read(cbuf)) != -1){
            osw.write(cbuf,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关流
        if (isr != null){

            try {
                isr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (osw != null){
            try {
                osw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

说明:文件编码的方式(比如:GBK),决定了解析时使用的字符集(也只能是GBK)

13.5.3 编码集

13.5.3.1 常见的编码表
  • ASCII:美国标准信息交换码,用一个字节的7位可以表示
  • ISO8859-1:拉丁码表。欧洲码表用一个字节的8位表示
  • GB2312:最多两个字节编码所有字符
  • GBK:中国的中文编码表升级,融合了更多的中文文字字符。最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节表示。
  • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpmrGSZS-1649637947285)(https://gitee.com/mclear/picgo/raw/master/img/20220411084103.png)]

说明:

  • 面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
  • Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。

UTF-8变长编码表示

image-20220411084113330

13.5.3.2 编码应用
  • 编码:字符串–>字节数组
  • 解码:字节数组–>字符串
  • 转换流的编码应用
    • 可以将字符按指定编码格式存储
    • 可以对文本数据按指定编码格式来解读
    • 指定编码表的动作由构造器完成

使用要求:

客户端/浏览器<—>后台(java,GO,Python,Node,js,php)<---->数据库

要求前前后后使用的字符集都要统一:UTF-8

13.6 标准输入、输出流

13.6.1 简介

System.in:标准的输入流。默认从键盘输入

System.out:标准的输出流,默认从控制台输出

13.6.2 主要方法

System类的setIn(InputStream is) 方式重新指定输入的流

System类的setOut(PrintStream ps) 方式重新指定输出的流

13.6.3 使用示例

从键盘输入字符串,要求将读取到的整行字符串大写输出。然后继续进行输入操作,直至当输入e 或者 exit 时,退出程序。

设计思路

方式一:使用Scanner 实现,调用next() 返回一个字符串

方法二:使用System.in实现。System.in -->转换流—>BufferedReader的readLine()

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        InputStreamReader isr = new InputStreamReader(System.in);
        br = new BufferedReader(isr);

        while (true) {
            System.out.println("请输入字符串:");
            String data = br.readLine();
            if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
                System.out.println("程序结束");
                break;
            }

            String upperCase = data.toUpperCase();
            System.out.println(upperCase);

        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.6.4 小练习

设计实现Scanner类

public class MyInput {
    // Read a string from the keyboard
    public static String readString() {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // Declare and initialize the string
        String string = "";
        // Get the string from the keyboard
        try {
            string = br.readLine();
        } catch (IOException ex) {
            System.out.println(ex);
        }
        // Return the string obtained from the keyboard
        return string;
    }
    // Read an int value from the keyboard
    public static int readInt() {
        return Integer.parseInt(readString());
    }
    // Read a double value from the keyboard
    public static double readDouble() {
        return Double.parseDouble(readString());
    }
    // Read a byte value from the keyboard
    public static double readByte() {
        return Byte.parseByte(readString());
    }
    // Read a short value from the keyboard
    public static double readShort() {
        return Short.parseShort(readString());
    }
    // Read a long value from the keyboard
    public static double readLong() {
        return Long.parseLong(readString());
    }
    // Read a float value from the keyboard
    public static double readFloat() {
        return Float.parseFloat(readString());
    }
}

13.7 打印流

PrintStreamPrintWriter 说明:

  • 提供了一系列重载的print() 和 println() 方法,用于多种数据类型的输出
  • System.out 返回的是 PrintStream 的实例
@Test
public void test2() {
    PrintStream ps = null;
    try {
        FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
        // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
        ps = new PrintStream(fos, true);
        if (ps != null) {// 把标准输出流(控制台输出)改成文件
            System.setOut(ps);//重新设置输出对象,把从控制台输出改成输出到文件
        }
        for (int i = 0; i <= 255; i++) { // 输出ASCII字符
            System.out.print((char) i);
            if (i % 50 == 0) { // 每50个数据一行
                System.out.println(); // 换行
            }
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ps != null) {
            ps.close();
        }
    }
}

13.8 数据流

DataInputStreamDataoutputStream **作用:**用于读取或写出基本数据类型的变量或字符串

示例代码:

讲内存中的字符串、基本数据类型的变量写出到文件中。

@Test
public void test3(){
    //1.造对象、造流
    DataOutputStream dos = null;
    try {
        dos = new DataOutputStream(new FileOutputStream("data.txt"));
        //数据输出
        dos.writeUTF("Bruce");
        dos.flush();//刷新操作,将内存的数据写入到文件
        dos.writeInt(23);
        dos.flush();
        dos.writeBoolean(true);
        dos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关闭流
        if (dos != null){
            try {
                dos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中。

/*
注意点:读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致!
 */
@Test
public void test4(){
    DataInputStream dis = null;
    try {
        //1.造对象、造流
        dis = new DataInputStream(new FileInputStream("data.txt"));
        //2.从文件读入数据
        String name = dis.readUTF();
        int age = dis.readInt();
        boolean isMale = dis.readBoolean();

        System.out.println("name:"+name);
        System.out.println("age:"+age);
        System.out.println("isMale:"+isMale);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关闭流
        if (dis != null){

            try {
                dis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.9 对象流

13.9.1 对象流:

ObjectInputStreamObjectOutputStream

13.9.2 作用

  • ObjectOutputStream:内存中的对象—>存储中的文件、通过网络传输出去:序列化过程
  • ObjectInputStream:存储中的文件、通过网络接收过来—>内存中的对象:反序列化过程

13.9.3 对象的序列化

  • 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而把这种二进制流持久的保存在磁盘中,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
  • 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
  • 序列化的是RMI(Remote Method Invoke-远程方法调用)过程的参数和返回值都必须实现的机制,RMI是JavaEE的基础。因此序列化机制是JavaEE平台的基础。
  • 如果需要让某个对象之处序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类时可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotserializableEXception 异常
    • Serializable
    • Externalizable
  • 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
    • private static final serialVersionUID;
    • serialVersionUID 用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本凡序列化时是否兼容
    • 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化。故建议显式声明。
  • 简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本的一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地的实体类serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException

13.9.4 实现序列化的对象所属的类需要满足:

  1. 需要实现接口:Serializable (标识接口)
  2. 当前类提供一个全局变量:serialVersionUID(序列版本号)
  3. 除了当前Person类需要实现Serializable接口之外,还须保证其内部所有属性是可序列化的。(默认情况下,基本数据类型可序列化)

补充:ObjectOutputStreamObjectInputStream 不能序列化statictransient修饰的成员变量

13.9.5 对象流的使用

13.9.5.1 序列化代码实现

序列化:将对象写入磁盘或进行网络传输

要求被序列化的对象必须实现序列化

@Test
public void testObjectOutputStream(){
    ObjectOutputStream oos = null;

    try {
        //1.创建对象,创建流
        oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
        //2.操作流
        oos.writeObject(new String("我爱北京天安门"));
        oos.flush();//刷新操作

        oos.writeObject(new Person("王铭",23));
        oos.flush();

        oos.writeObject(new Person("张学良",23,1001,new Account(5000)));
        oos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(oos != null){
            //3.关闭流
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
13.9.5.2 反序列化代码实现

反序列化:将磁盘的对象数据源读出

@Test
public void testObjectInputStream(){
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("object.dat"));
        Object obj = ois.readObject();
        String str = (String) obj;

        Person p = (Person) ois.readObject();
        Person p1 = (Person) ois.readObject();

        System.out.println(str);
        System.out.println(p);
        System.out.println(p1);

    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } finally {
        if(ois != null){
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.10 任意存取文件流

RandomAccessFile 的使用

13.10.1 简介

  • RandomAccessFile直接继承于java.lang.Object 类,实现了DataInputDataOutput 接口
  • RandomAccessFile 既可以作为一个输入流,又可以作为一个输出流
  • RandomAccessFile 类支持"随机访问"的方式,程序可以直接跳到文件的任意地方来读、写文件
    • 支持只访问文件的部分内容
    • 可以向已存在的文件后追加内容
  • RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。
  • RandomAccessFile 类对象可以自由移动指针:
    • long getFilePointer():获取文件记录指针的当前位置
    • void seek(long pos):将文件记录指针定位到pos位置

构造器

public RandomAccessFile(File file , String mode)

public RandomAccessFile(String name , String mode)

13.10.2 使用说明:

  1. 如果 RandomAccessFile 作为输出流时,写出到的文件如果不存在,则执行过程中自动创建。
  2. 如果写出到的文件存在,则会对原文件内进行覆盖。(默认情况喜爱,从头覆盖)
  3. 可以通过相关的操作,实现RandomAccessFile 插入 数据的效果
  4. 创建RandomAccessFile 类实例需要指定一个 mode参数,该参数指定RandomAccessFile的访问模式:
    • r:以只读方式打开
    • rw:打开以便读取和写入
    • rwd:打开以便读取和写入;同步文件内容的更新
    • rws:打开以便读取和写入;同步文件内容和原数据的更新
  5. 如果模式为只读r,则不会创建文件,而是会去读取一个已经存在的文件,读取的文件不存在则会出现异常。如果模式为rw读写,文件不存在则会去创建文件,存在则不会创建。(“rw"模式,数据不会立即写到磁盘中;而"rwd”,数据会立即写入到磁盘。如果写数据过程发生异常,"rwd"模式中已被write的数据被保存到磁盘,而"rw"的全部丢失)

13.10.3 使用示例

文件的读取和写出操作

@Test
public void test1() {

    RandomAccessFile raf1 = null;
    RandomAccessFile raf2 = null;
    try {
        //1.创建对象,创建流
        raf1 = new RandomAccessFile(new File("test.jpg"),"r");
        raf2 = new RandomAccessFile(new File("test1.jpg"),"rw");
        //2.操作流
        byte[] buffer = new byte[1024];
        int len;
        while((len = raf1.read(buffer)) != -1){
            raf2.write(buffer,0,len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关闭流
        if(raf1 != null){
            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(raf2 != null){
            try {
                raf2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

使用RandomAccessFile 实现数据的插入效果

@Test
public void test2(){
    RandomAccessFile raf1 = null;
    try {
        raf1 = new RandomAccessFile(new File("hello.txt"), "rw");

        raf1.seek(3);//将指针调到角标为3的位置
        //            //方式一
        //            //保存指针3后面的所有数据到StringBuilder中
        //            StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
        //            byte[] buffer = new byte[20];
        //            int len;
        //            while ((len = raf1.read(buffer)) != -1){
        //                builder.append(new String(buffer,0,len));
        //            }

        //方式二
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[20];
        int len;
        while ((len = raf1.read(buffer)) != -1){
            baos.write(buffer);
        }
        //调回指针,写入“xyz”
        raf1.seek(3);
        raf1.write("xyz".getBytes());
        //将StringBuilder中的数据写入到文件中
        raf1.write(baos.toString().getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (raf1 != null){
            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

13.11 流的基本应用总结

  • 流是用来处理数据的。
  • 处理数据时,一定要先明确数据源,与数据的目的地。数据源可以是文件,可以是键盘数据,目的地,可以是文件、显示器或者其他设备
  • 而流只是在帮助数据进行传输,并对传输的额数据进行处理,比如过滤处理、转换处理等。
  • 除去RandomAccessFile类外所有的流都继承于四个基本数据流抽象类InputStream、OutputStream、Reader、Writer
  • 不同的操作流对应的后缀为四个抽象基类中的某一个

image-20220411084130742

  • 不同处理流的使用方式都是标准操作:
    • 创建文件对象,创建相应的流
    • 处理流数据
    • 关闭流
    • 用try-catch-finally

13.12 NIO

Path、Paths、File的使用比较简单,后期会再抽时间详细写有关NIO的博客

13.12.1 NIO的使用说明:

  • Java NIO(New IO,Non-Blocking IO)是从Java 1.4 版本开始引入的一套新的IO API,可以替代标准的Java IO AP。
  • NIO与原来的IO同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于同道的IO操作
  • NIO将以更高效地方式进行文件的读写操作
  • JDK 7.0对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,称他为NIO.2。
Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO
|-----java.nio.channels.Channel
      |---- FileChannel:处理本地文件
      |---- SocketChannel:TCP网络编程的客户端的Channel
      |---- ServerSocketChannel:TCP网络编程的服务器端的Channel
      |---- DatagramChannel:UDP网络编程中发送端和接收端的Channel

13.12.2 Path接口—JDK 7.0 提供

  • 早期的Java只提供了一个File 类来访问文件系统,但File 类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。
  • NIO.2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是File类的升级版本,实际引用的资源也可以不存在。
13.12.2.1 Path的说明:

Path替换原有的File类。

  • 在以前IO操作都是这样写的:
    • import java.io.File
    • File file = new File("index.html")
  • 但在Java7中,我们可以这样写:
    • import java.nio.file.Path;
    • import java.nio.file.Paths;
    • Path path = Paths.get("index.html")
13.12.2.2 Paths的使用
  • Paths类提供的静态get()方法用来获取Path对象
  • static Path get(String first, String "".more):用于将多个字符串串连成路径
  • static Path get(URI , uri):返回指定uri对应的Path路径

代码示例

@Test
public void test1(){
    Path path1 = Paths.get("hello.txt");//new File(String filepath)

    Path path2 = Paths.get("E:\\", "test\\test1\\haha.txt");//new File(String parent,String filename);

    Path path3 = Paths.get("E:\\", "test");

    System.out.println(path1);
    System.out.println(path2);
    System.out.println(path3);
}
13.12.2.3 常用方法
  • String toString():返回调用 Path 对象的字符串表示形式
  • boolean startsWith(String path):判断是否以path路径开始
  • boolean endsWith(String path):判断是否以path路径结束
  • boolean isAbsolute():判断是否是绝对路径
  • Path getParent():返回Path对象包含整个路径,不包含Path对象指定的文件路径
  • Path getRoot():返回调用Path 对象的根路径
  • Path getFileName():返回与调用Path对象关联的文件名
  • int getNameCount():返回Path根目录后面元素的数量
  • Path getName(int idx):返回指定索引位置idx的路径名称
  • Path toAbsolutePath():作为绝对路径返回调用Path对象
  • Path resolve(Path p) :合并两个路径,返回合并后的路径对应的 Path 对象
  • File toFile(): 将 Path 转化为 File 类的对象
@Test
public void test2() {
    Path path1 = Paths.get("d:\\", "nio\\nio1\\nio2\\hello.txt");
    Path path2 = Paths.get("hello.txt");
    //		String toString() : 返回调用 Path 对象的字符串表示形式
    System.out.println(path1);

    //		boolean startsWith(String path) : 判断是否以 path 路径开始
    System.out.println(path1.startsWith("d:\\nio"));
    //		boolean endsWith(String path) : 判断是否以 path 路径结束
    System.out.println(path1.endsWith("hello.txt"));
    //		boolean isAbsolute() : 判断是否是绝对路径
    System.out.println(path1.isAbsolute() + "~");
    System.out.println(path2.isAbsolute() + "~");
    //		Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
    System.out.println(path1.getParent());
    System.out.println(path2.getParent());
    //		Path getRoot() :返回调用 Path 对象的根路径
    System.out.println(path1.getRoot());
    System.out.println(path2.getRoot());
    //		Path getFileName() : 返回与调用 Path 对象关联的文件名
    System.out.println(path1.getFileName() + "~");
    System.out.println(path2.getFileName() + "~");
    //		int getNameCount() : 返回Path 根目录后面元素的数量
    //		Path getName(int idx) : 返回指定索引位置 idx 的路径名称
    for (int i = 0; i < path1.getNameCount(); i++) {
        System.out.println(path1.getName(i) + "*****");
    }
    //		Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
    System.out.println(path1.toAbsolutePath());
    System.out.println(path2.toAbsolutePath());
    //		Path resolve(Path p) :合并两个路径,返回合并后的路径对应的Path对象
    Path path3 = Paths.get("d:\\", "nio");
    Path path4 = Paths.get("nioo\\hi.txt");
    path3 = path3.resolve(path4);
    System.out.println(path3);

    //		File toFile(): 将Path转化为File类的对象
    File file = path1.toFile();//Path--->File的转换
    Path newPath = file.toPath();//File--->Path的转换

}

13.12.3 Files类

java.nio.fileFiles 用于操作文件或目录的工具类

13.12.3.1 File类常用方法
  • Path copy(Path src, Path dest, CopyOption ...how):文件的复制要想复制成功,要求path1对应的物理上的文件存在。path1对应的文件没有要求。

  • Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);

  • Path createDirectory(Path path, FIleAttribute<?> ...attr);创建一个目录

    要想执行成功,要求path对应的俄物理上的文件目录不存在。一旦存在,抛出异常。

  • Path createFile(Path path, FileAttribute<?> ...arr); 创建一个文件

    要想执行成功,要求path对应的物理上的文件不存在。一旦存在,抛出异常。

  • void delete(Path path):删除一个文件/目录,如果不存在,执行报错。

  • void deleteIfExists(Path path)path 对应的文件/目录如果存在,执行删除。如果不存在,正常执行结束

  • Path move(Path src, Path dest , CopyOption...how):将src移动到dest位置

    要想执行成功,src对应的物理上的文件需要存在,dest对应的文件没有要求。

  • long size(Path path):返回path指定文件的大小

@Test
public void test1() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");
    Path path2 = Paths.get("atguigu.txt");

    //		Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
    //要想复制成功,要求path1对应的物理上的文件存在。path1对应的文件没有要求。
    //		Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);

    //		Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
    //要想执行成功,要求path对应的物理上的文件目录不存在。一旦存在,抛出异常。
    Path path3 = Paths.get("d:\\nio\\nio1");
    //		Files.createDirectory(path3);

    //		Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
    //要想执行成功,要求path对应的物理上的文件不存在。一旦存在,抛出异常。
    Path path4 = Paths.get("d:\\nio\\hi.txt");
    //		Files.createFile(path4);

    //		void delete(Path path) : 删除一个文件/目录,如果不存在,执行报错
    //		Files.delete(path4);

    //		void deleteIfExists(Path path) : Path对应的文件/目录如果存在,执行删除.如果不存在,正常执行结束
    Files.deleteIfExists(path3);

    //		Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
    //要想执行成功,src对应的物理上的文件需要存在,dest对应的文件没有要求。
    //		Files.move(path1, path2, StandardCopyOption.ATOMIC_MOVE);

    //		long size(Path path) : 返回 path 指定文件的大小
    long size = Files.size(path2);
    System.out.println(size);

}
13.12.3.2 Files类常用方法:用于判断
  • boolean exists(Path path, LinkOption ...opts):判断文件是否存在

  • boolean isDirectory(Path path, LinkOption ...opts):判断是否是目录

    不要求此path对应的物理文件存在。

  • boolean isRegularFile(Path path, LinkOption...opts):判断是否是文件

  • boolean isHidden(Path path):判断是否是隐藏文件

    要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。

  • boolean isReadable(Path path):判断文件是否可读

  • boolean isWritable(Path path):判断文件是否可写

  • boolean notExists(Path path, LinkOption ...opts):判断文件是否不存在

代码示例

@Test
public void test2() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");
    Path path2 = Paths.get("atguigu.txt");
    //		boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
    System.out.println(Files.exists(path2, LinkOption.NOFOLLOW_LINKS));

    //		boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
    //不要求此path对应的物理文件存在。
    System.out.println(Files.isDirectory(path1, LinkOption.NOFOLLOW_LINKS));

    //		boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件

    //		boolean isHidden(Path path) : 判断是否是隐藏文件
    //要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。
    //		System.out.println(Files.isHidden(path1));

    //		boolean isReadable(Path path) : 判断文件是否可读
    System.out.println(Files.isReadable(path1));
    //		boolean isWritable(Path path) : 判断文件是否可写
    System.out.println(Files.isWritable(path1));
    //		boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
    System.out.println(Files.notExists(path1, LinkOption.NOFOLLOW_LINKS));
}

补充:

  • StandardOpenOption.READ:表示对应的Channel是可读的。
  • StandardOpenOption.WRITE:表示对应的Channel是可写的
  • StandardOpenOption.CREATE:如果要写出的文件不存在,则创建。如果存在,忽略
  • StandardOpenOption.CREATE_NEW:如果要写出的文件不存在,则创建。如果存在,抛异常。
13.12.3.3 Files类常用方法:用于操作内容
  • InputStream newInputStream(Path path, OpenOption ...how):获取InputStream对象
  • OutputStream newOutputStream(Path path, OpenOption ...how):获取OutputStream 对象
  • SeekableByteChannel newByteChannel(Path path, OpenOption...how):获取与指定文件的连接,how 指定打开方式。
  • DirectoryStream< Path > newDirectoryStream(Path path):打开path指定的目录
@Test
public void test3() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");

    //		InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
    InputStream inputStream = Files.newInputStream(path1, StandardOpenOption.READ);

    //		OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
    OutputStream outputStream = Files.newOutputStream(path1, StandardOpenOption.WRITE,StandardOpenOption.CREATE);


    //		SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
    SeekableByteChannel channel = Files.newByteChannel(path1, StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);

    //		DirectoryStream<Path>  newDirectoryStream(Path path) : 打开 path 指定的目录
    Path path2 = Paths.get("e:\\teach");
    DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path2);
    Iterator<Path> iterator = directoryStream.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}

e(Path src, Path dest , CopyOption…how):将src移动到dest`位置

要想执行成功,src对应的物理上的文件需要存在,dest对应的文件没有要求。

  • long size(Path path):返回path指定文件的大小
@Test
public void test1() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");
    Path path2 = Paths.get("atguigu.txt");

    //		Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
    //要想复制成功,要求path1对应的物理上的文件存在。path1对应的文件没有要求。
    //		Files.copy(path1, path2, StandardCopyOption.REPLACE_EXISTING);

    //		Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
    //要想执行成功,要求path对应的物理上的文件目录不存在。一旦存在,抛出异常。
    Path path3 = Paths.get("d:\\nio\\nio1");
    //		Files.createDirectory(path3);

    //		Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
    //要想执行成功,要求path对应的物理上的文件不存在。一旦存在,抛出异常。
    Path path4 = Paths.get("d:\\nio\\hi.txt");
    //		Files.createFile(path4);

    //		void delete(Path path) : 删除一个文件/目录,如果不存在,执行报错
    //		Files.delete(path4);

    //		void deleteIfExists(Path path) : Path对应的文件/目录如果存在,执行删除.如果不存在,正常执行结束
    Files.deleteIfExists(path3);

    //		Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
    //要想执行成功,src对应的物理上的文件需要存在,dest对应的文件没有要求。
    //		Files.move(path1, path2, StandardCopyOption.ATOMIC_MOVE);

    //		long size(Path path) : 返回 path 指定文件的大小
    long size = Files.size(path2);
    System.out.println(size);

}
13.12.3.2 Files类常用方法:用于判断
  • boolean exists(Path path, LinkOption ...opts):判断文件是否存在

  • boolean isDirectory(Path path, LinkOption ...opts):判断是否是目录

    不要求此path对应的物理文件存在。

  • boolean isRegularFile(Path path, LinkOption...opts):判断是否是文件

  • boolean isHidden(Path path):判断是否是隐藏文件

    要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。

  • boolean isReadable(Path path):判断文件是否可读

  • boolean isWritable(Path path):判断文件是否可写

  • boolean notExists(Path path, LinkOption ...opts):判断文件是否不存在

代码示例

@Test
public void test2() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");
    Path path2 = Paths.get("atguigu.txt");
    //		boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
    System.out.println(Files.exists(path2, LinkOption.NOFOLLOW_LINKS));

    //		boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
    //不要求此path对应的物理文件存在。
    System.out.println(Files.isDirectory(path1, LinkOption.NOFOLLOW_LINKS));

    //		boolean isRegularFile(Path path, LinkOption … opts) : 判断是否是文件

    //		boolean isHidden(Path path) : 判断是否是隐藏文件
    //要求此path对应的物理上的文件需要存在。才可判断是否隐藏。否则,抛异常。
    //		System.out.println(Files.isHidden(path1));

    //		boolean isReadable(Path path) : 判断文件是否可读
    System.out.println(Files.isReadable(path1));
    //		boolean isWritable(Path path) : 判断文件是否可写
    System.out.println(Files.isWritable(path1));
    //		boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
    System.out.println(Files.notExists(path1, LinkOption.NOFOLLOW_LINKS));
}

补充:

  • StandardOpenOption.READ:表示对应的Channel是可读的。
  • StandardOpenOption.WRITE:表示对应的Channel是可写的
  • StandardOpenOption.CREATE:如果要写出的文件不存在,则创建。如果存在,忽略
  • StandardOpenOption.CREATE_NEW:如果要写出的文件不存在,则创建。如果存在,抛异常。
13.12.3.3 Files类常用方法:用于操作内容
  • InputStream newInputStream(Path path, OpenOption ...how):获取InputStream对象
  • OutputStream newOutputStream(Path path, OpenOption ...how):获取OutputStream 对象
  • SeekableByteChannel newByteChannel(Path path, OpenOption...how):获取与指定文件的连接,how 指定打开方式。
  • DirectoryStream< Path > newDirectoryStream(Path path):打开path指定的目录
@Test
public void test3() throws IOException{
    Path path1 = Paths.get("d:\\nio", "hello.txt");

    //		InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象
    InputStream inputStream = Files.newInputStream(path1, StandardOpenOption.READ);

    //		OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象
    OutputStream outputStream = Files.newOutputStream(path1, StandardOpenOption.WRITE,StandardOpenOption.CREATE);


    //		SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
    SeekableByteChannel channel = Files.newByteChannel(path1, StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);

    //		DirectoryStream<Path>  newDirectoryStream(Path path) : 打开 path 指定的目录
    Path path2 = Paths.get("e:\\teach");
    DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path2);
    Iterator<Path> iterator = directoryStream.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值