自学javase回顾(9/10)

1、集合简述和Collection接口

2、List接口和Set接口

3、Map接口

4、IO流


一、集合简述

一、集合概念:集合全部在java.util.包下

在这里插入图片描述

为什么类/接口一定要去实现/继承Iterable这个接口呢? 而不直接实现Iterator接口呢?
看一下JDK中的集合类,比如List一族或者Set一族,
都是继承了Iterable接口,但并不直接继承Iterator接口。
①仔细想一下这么做是有道理的。因为Iterator接口的核心方法next()或者hasNext()是依赖于迭代器的当前迭代位置的。
如果Collection直接继承Iterator接口,势必导致集合对象中包含当前迭代位置的数据(指针)。(即当前迭代器没有迭代完数据还残留又被别的类拿去迭代了)
当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么next()方法的结果会变成不可预知。
除非再为Iterator接口添加一个reset()方法,用来重置当前迭代位置。
但即时这样,Collection也只能同时存在一个当前迭代位置。
而Iterable则不然,每次调用都会返回一个从头开始计数的迭代器。
多个迭代器是互不干扰的。
②每一种集合类实现Iterable接口所返回的Iterator具体类型可能不同,Array可能返回ArrayIterator,Set可能返回SetIterator,Tree可能返回TreeIterator,但是它们都实现了Iterator接口,因此,客户端不关心到底是哪种Iterator,它只需要获得这个Iterator接口即可,这就是面向对象的威力。
③Java SE5引入了Iterable接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。
for-each循环可以与任何实现了Iterable接口的对象一起工作。而java.util.Collection接口继承java.lang.Iterable,故标准类库中的任何集合都可以使用for-each循环。
因此你创建了任何实现Iterable的自定义类,都可以将它用于foreach语句中。(这就是为什么集合能用foreach原因)
Iterable的主要作用为:实现Iterable接口来实现适用于foreach遍历的自定义类。

具体深入iterator与iterable看: http://www.cnblogs.com/highriver/archive/2011/07/27/2077913.html
https://www.cnblogs.com/softidea/p/5167676.html
https://blog.csdn.net/yujin753/article/details/44756569
https://www.cnblogs.com/keyi/p/5821285.html

回归正题:
1、什么是集合?(属于引用类型,也有自身内存地址)
①回顾数组自身就是小集合,但只能存“单种数据类型”的容器[限定长度不可变。
②集合是能够存储“多种数据类型”的容器,且不限定长度,长度可变。
相比较数组来说,更加灵活。

2、集合为什么说在开发中使用较多????
①集合是一个能一次容纳多个对象类型的载体容器。
②在实际开发中,假设连接数据库,数据库当中有10条记录,那么把这10条记录都要查询出来。
在java程序中会将10条数据封装成10个不同的对象,然后将10个封装好的java对象放到某个集合中,
再将集合传到前端,然后前端遍历集合,将数据一个一个展现在客户面前。
③但是实际开发一个集合一般都存同一数据,会使用到“泛型”指定数据类型

3、集合当中不能直接储存基本数据类型,也不能直接储存java引用对象。!!!
(数组本身自己有一个引用(栈)地址,他的内部每一个元素盒子也有对应(堆)地址,然后存的元素如果是引用类型也是存的引用地址。三个地址。)
①集合如要存基本数据类型,会自动装箱为引用Integer等包装类,才能存进去集合。 List.add(100);—>//100自动装箱为Integer包装类
②集合如要存引用类型,其实也都是java对象的内存地址。(或者说集合中存的是引用地址,指向对象内存地址)
③因此总结,集合任何时候存的都是“引用”类型。

4、在java中,每个不同的集合,底层会对应不同的数据结构(数组、二叉树、链表、哈希表等)。

【1】在Java集合接口中,所有元素都是可迭代,可遍历的。(都继承了Iterable接口)
【2】复习:同种类叫继承extends、不同种类叫实现implements(类继承类、接口继承接口、类实现接口)。
1、Collection 接口(List和Set)、Map接口 is a Iterable ----> 继承、泛化关系
2、List接口、Set子接口 is a 父Collection接口 -----> 继承、泛化关系
3、ArrayList类、LinkList类、Vector类 like a List接口 -----> 实现关系
4、HashSet类、TreeSet类 like a Set接口-----> 实现关系
5、HashMap类、HashTable类、TreeMap类 like a Map接口 -----> 实现关系

在这里插入图片描述

二、集合分类:(Collection【数组和双链表】和Map【二叉树和哈希表】):
1、Collection接口,特点:
(以单个(一个一个存)的方式进行存储对象内存地址)
Collection作为超级父接口,有2个常用的子接口:
(1)List接口,特点:放有序可重复的元素,元素带有下标(注:这里的有序不代表从小到大,而是存储进去的顺序和取出来的顺序一致)
List接口中有3个常用的子类:
[1]ArrayList类:是非线程安全的,底层采用了数组的数据结构
[2]LinkList类:底层采用了双向链表的数据结构
[3]Vector类:是线程安全的,底层采用了数组的数据结构(现在使用较少,因为其中所有的方法带有synchronized同步关键字,效率较低)

(2)Set接口,特点:放无序不可重复元素,元素没有下标
Set接口中有2个常用的子类:(注意:有关Set集合所存的东西,底层都会存到对应的Map集合去。)

[1]HashSet类:,实际上底层new了一个HashMap类(即实际是存到HashMap集合中的,底层采用了哈希表)。
[2]TreeSet类:,实际上底层new了一个TreeMap类(即实际是存到TreeMap集合中的,底层采用了二叉树数据结构)。
TreeSet类它的上面还实现了另一个父接口:SortedSet接口。这个接口让TreeSet它有一个特点:可以从小到大自动排序。

Set注意事项:
①虽然Set集合所存的数据,底层都会把它们存去Map集合中的key盒子中,但是两者还是有区别的。
②Set集合和Map集合区别:Set只能单个存元素,Map以键值对(key-value)存元素,但特点都是:存无序不可重复的元素,无下标。

2、Map集合接口:
【1】特点:①(以key-value键值对儿(一对一对存)的方式进行存储对象内存地址)
②存储的key元素不能重复,但对应的value可以重复,但是一个key只能对应一个value.

【2】Map接口中,有3个常用的子类:
(1)HashMap:非线程安全的,底层是哈希表
(2)TreeMap:底层是二叉树,它的上面还有一个父接口:SortedMap,因此也可以排序
(3)HashTable:线程安全的,底层是哈希表(现在使用较少,因为其中所有的方法带有synchronized关键字,效率较低,控制线程有更好的方案)
注意:HashTable有一个子类Properties类:(也是Map集合一种,也是线程安全的),Properties类称为属性类,只能是以key-value键值对儿存储“String字符串”,大多用于充当配置文件让IO流读取)

【3】Map注意事项:
key是属性关键字、value是值
key-value分布式存储系统查询速度快、存放数据量大、支持高并发,非常适合通过key进行查询value,但不能进行复杂的条件查询。


二、详解Collection父接口:

1、Collection能存储什么数据?
①在没有使用泛型之前,Collection可以存储Object下所有的子类型。(注意:存的都是对象内存地址。)
②使用了泛型之后,Collection只能存储泛型所指定的类。

注意点:Collection、List、Set仅仅只是一个抽象接口,因此当我们具体需要使用这些集合的时候,我们需要创建new一个该接口的实现类。(即多态要去面向接口编程)

2、Collection接口有哪些常用的方法?(注意:这些方法都是共有的,Collection是List和Set的爸爸,是ArrayList或者HashSet的爷爷)

1、add方法:向集合中直接添加元素(任何元素)
2、size方法:查看集合中元素的个数, 返回int类型
(注意:size方法只是查看当前集合的元素个数,不是看元素的初始化容量)
因为底层数组他没有设置length属性,因此只有size方法是但只是用于查看集合里面的元素个数,因此不是代表底层Object数组的动态初始化容量!!!

3、remove方法:从集合中删除某个元素,不代表这个元素消失了,只是从集合中拉出来了。
4、clear方法:从集合中清空该集合的所有元素
5、contains方法:查看集合中是否包含存在这个元素, 返回布尔类型 c.contains(1)注意:该类已经重写了equals方法,因此contains比较的是具体内容是否包含即可,不比较内存地址
6、isEmpty方法:集合中是否为空 , 返回布尔类型
7、toArray方法:将集合转换为数组

3、怎么遍历Collection中的所有元素?【有两种方法:用其Collection(包括List和Set)通用的Iterator迭代器类或者foreach循环器】。

PS:Map集合没有自己的迭代器Iterator,因为没有我看过源代码没有继承 Iterable接口,只能间接遍历,后面具体讲。

public interface Map<K,V> {}
public interface Collection<E> extends Iterable<E> {}

1、Iterator构建迭代器 + while循环。

(Collection接口继承 Iterable接口,因此调Iterable接口的iterator方法返回一个迭代器Iterator对象,就可以拿去遍历迭代)):
迭代器类中的常用方法:
先获取迭代器对象it:Iterator it = 集合引用名.iterator();
(1)判断:it.hasNext()代表下次迭代是否还有元素,返回boolean布尔类型。如果仍有元素可以遍历,则返回true
(2)取值:it.next() 指向下一个元素;并且一定要返回一个Object类型的该自身元素
(3)删除:it.remove移除迭代器指向的元素;
重点:在用迭代器iterator遍历迭代元素的过程过,如果想要删除元素,一定要使用迭代器的remove方法,不要使用集合自身的remove方法,否则报错,ConcurrentModificationException 修改结构异常。(区别在下面详细讲)

2、foreach循环器:(因为源代码中Collection是继承了Iterable接口的所以可以使用foreach)

foreach循环遍历数组作用;(更加快速简洁)
foreach循环概念,这是一种更加便利的循环工具,循环出数组或者集合中的每一个元素
foreach循环语法:(遍历的新集合变量名是自己定义的。)
①一维数组;
for(元素的类型 遍历出来的新集合变量名:要被遍历数组名或者集合名){
System.out.println(遍历的变量名);
②二维数组;
for(元素的类型[] 遍历的变量名:数组名或者集合名){
for(元素的类型 遍历的变量名:数组名或者集合名){
System.out.println(遍历的变量名);

package Collection集合.Collection集合父接口;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Collection父接口常用方法 {
    public static void main(String[] args) {

        //下面是使用了泛型的例子,指定只能放Integer<类型>。
        Collection<Integer> c = new ArrayList<>();  //接口不能直接new对象,但是能多态间接new子类对象。
        c.add(1); //不能直接存基数类型,这里是自动装箱Integer了
        c.add(3);
        System.out.println(c.isEmpty());//false
        System.out.println(c.contains(1));//true  具体值1包含在里面
        c.remove(1);                  //删除元素1
        System.out.println(c.contains(1));//false   因为1已经被删除

        c.clear();                    //清空所有元素
        System.out.println(c.size());//清空之后都为,0

        c.add(45);//清空之后再加元素45。
        c.add(44);
        c.add(43);
        Object[] i = c.toArray();//把整个集合的元素转换为引用数组形式,注意一定返回的是Object引用类型。
                               // (因为全部引用对象的父类都是Object,可以万金油,这样集合放什么类型都不会返回错误的类型)
        /**
         * Object/int.. 是要遍历的数组类型(固定)
         * o  每次被遍历出来元素的变量名(自己新定义的)
         * i  被遍历的数组名或者集合名(固定)
         */
        for (Object o : i) {   //把该数组遍历出来即可
            System.out.println(o);//45 44 43
        }
    }


    //一:下面是使用了迭代器Iterator来把泛型的Collection遍历
    public static void  Iterator迭代器() {
        Collection<Integer> l = new ArrayList<>();
        l.add(1);
        l.add(2);
        Iterator it = l.iterator();
        while (it.hasNext()) {  //true代表还有下一个元素可以遍历。 判断一个,取一个,直到为false没有下一个元素可遍历
            Object i = it.next(); //输出下一个元素,并接收该元素。
            System.out.println(i);//1 2
        }
    }


    //二:下面是使用了foreach来把泛型的Collection遍历
    public static void  foreach遍历() {
        Collection<Integer> l = new ArrayList<>();
        l.add(1);
        l.add(2);
        for (Integer i : l) {
            System.out.println(i);//1 2
        }
    }
}

4、遍历Collection的List集合特点(遍历多态集合!):
放有序可重复的元素,元素带有下标(注:这里的有序不代表从小到大,而是存储进去的顺序和取出来的顺序一致)

package Collection集合.Collection集合父接口;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class 遍历Collection的List集合特点 {
    public static void main(String[] args) {
        //注意,以下讲解的遍历方式或者迭代方式,是所有Collection(包括List和Set)通用的一种方式。
        //在Map集合中不能用。
        //这是不用泛型的遍历例子、间接创建集合对象。
        //特点;List放有序可重复的元素、底层是用数组结构储存的。
        Collection c = new ArrayList(); //用LinkedList或Vector都行
        //多态间接new对象。(List extends  Collection --->List l = ArrayList();)。
        //添加元素
        c.add(100);//这里100已经自动装箱为包装类,因此也是一个类对象。
        c.add("abc");//String字符串也是一个对象
        c.add("123");
        c.add(new Object());



        //对集合Collection中进行遍历迭代
        //通过迭代器Iterator遍历集合元素
        // 第一步:获取Collection集合对象中的迭代器对象Iterator。
        // 获取途径:(因为Collection接口继承 Iterable接口,调Iterable接口的iterator方法返回一个迭代器Iterator对象,就可以遍历迭代)
        Iterator i = c.iterator();      //拿迭代器对象。
        while(i.hasNext()){           //判断是否有下一个元素  。  (如果放true,一直取元素会报异常NoSuchElementException)
            Object o = i.next();     //取出该一个元素。

            //分别判断上面元素为什么类型
            if(o instanceof Integer){
                System.out.println(o+"为Integer类型"); //100
            }if(o instanceof String){
                System.out.println(o+"为String类型"); //123和abc
            }if (o instanceof Object){
                System.out.println(o+"为Object类型");//100、123和abc、new Object()全是Object。因此写Object可满足所有。
            }

            System.out.println(o);//100、abc、123、java.lang.Object@1540e19d(List放有序可重复的元素、取出来顺序等于存进去的顺序)
                                //注意此时100还是Integer类型,abc、123还是String类型
                               //  只是转换为字符串输出了。因为Integer和String类型已经重写好了toString方法。
        }
    }
}
100为Integer类型
100为Object类型
100
abc为String类型
abc为Object类型
abc
123为String类型
123为Object类型
123
java.lang.Object@1540e19d为Object类型
java.lang.Object@1540e19d

5、遍历Collection的Set集合特点:
放无序不可重复元素,元素没有下标
(注意:有关Set集合所存的东西,底层都会存到对应的Map集合去。)

package Collection集合.Collection集合父接口;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class 遍历Collection的Set集合特点 {
    public static void main(String[] args) {
        //new的是HashSet、但实际上底层new了一个HashMap类(即实际是存到HashMap集合中的,底层采用了哈希表)
        //Set放无序不可重复元素,元素没有下标
        Collection c1 = new HashSet(); //接口不能直接new对象。多态子类实现父类接口。(Set extends  Collection --->Set s = new HashSet();)
        c1.add(100);
        c1.add(200);
        c1.add(300);
        c1.add(400);
        c1.add(100); //重复不能存进去。。。这是new的Set子接口

        //遍历集合元素,先创建迭代器对象。(通过集合的iterator()方法返回一个迭代器)
        Iterator i = c1.iterator();
        while (i.hasNext()){
            System.out.println( i.next());//400、100、200、300  无序不可重复元素
        }
    }
}

6、Collection中contains和remove方法详解:

一、
测试contains方法:查看集合中是否包含存在这个元素,
返回布尔类型   c.contains(1)        ( 比较的是xx集合的具体内容中是否包含xx的具体内容)
注意:
在源代码中,该集合类已经重写了equals方法,因此:
①如果放进去的对象元素也是已经写好或重写好equals方法的话
那么contains比较的是(两个对象的)具体内容是否包含即可,不比较内存地址。
②如果放进去的对象元素不重写equals方法的话,那么一个比内容,一个比地址肯定是不包含的。


二、测试remove方法:删除某个元素,和包含contain方法一样
在源代码中,该集合类也已经重写了equals方法,因此:
①如果放进去比的元素也是已经写好或重写好equals方法的话那么remove方法,删的是具体内容的值。
因此删s2等于删s1,因为重写了equals,删的是对象具体内容。即使声明不一样,但是内容一样可以代替去删
②如果放进去的对象元素不重写equals方法的话,也可以直接删,只是删了整个地址并且不能代替了


三、测试了这么多,总结;
③储存在集合中的任何引用类型,都一定一定一定!!!要重写好该类型的equals方法。
(String类或者Integer基本包装类已经重写好,可以不用,其他都要)

*/

package Collection集合.Collection集合父接口;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;

public class Collection中contains和remove方法详解 {
    public static void main(String[] args) {

        //放String类(sun已重写好equals)进去看是否包含.
        Collection c = new ArrayList();
        String s1 = new String("abc"); //两个对象。一个在堆一个在方法区常量池
        String s2 = new String("def"); //两个对象
        c.add(s1);
        c.add(s2);
        System.out.println("元素的个数是"+c.size()); // 2。集合堆内存盒子,储存两个元素。只存堆对象内存地址。(因此这里存两个堆对象内存地址)

        String x = new String("abc");
        System.out.println(c.contains(x)); //很重要!!!true   c的具体内容中是否包含x的具体内容。"abc" = "abc"
        //从源代码可知;该集合类已经提早重写了equals方法,又因为放进去比的元素即String类也已经提早重写了equals方法,
        // 因此死记;contains方法比较的是(两个对象的)具体内容是否包含即可,不要去比较内存地址。

        doUser();
        doInteger();
        doRemove();
    }

    //放Integer类(sun已重写好equals)进去看是否包含.
    public static void doInteger() {
        Collection c1 = new ArrayList();
        Integer i1 = new Integer(123);
        Integer i2 = new Integer(123);
        Integer i3 = 123;
        c1.add(i1);
        System.out.println(c1.contains(i2)); //true
        System.out.println(  c1.contains(i3));//true


    }


    //放User类(要自己重写equals)进去看是否包含.
    public static void doUser(){
        Collection c2 = new ArrayList();
        User u1 =   new User("涂岳新");
        User u2 =   new User("涂岳新");

        c2.add(u1);//把u1对象 添加到集合中
        System.out.println(c2.contains(u2)); //true
       //从源代码可知;该集合类已经提早重写了equals方法,又因为放进去比的元素即User类也已经提早重写了equals方法,
        //因此( 比较的是xx集合的具体内容中是否包含xx的具体内容,不比较内存地址)
    }

    //删除String类(已经重写好了),看是否删除
    public static void doRemove(){
        Collection c3 = new ArrayList();
        String s1 = new String("123");
        String s2 = new String("123");
        c3.add(s1); //放s1进去
        System.out.println( c3.size());//1
        c3.remove(s2); //删s2等于删s1,因为重写了equals,删的是对象具体内容。声明不一样,但是内容一样可以代替去删
        System.out.println(c3.size());//0


    }
}


class User{
    private String name;

    public User(){

    }
    public User(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}


7、Collection集合自身和iterator迭代器的remove方法区别:
结论:
①在用迭代器iterator遍历迭代元素的过程过,如果想要删除元素,一定要使用迭代器的remove方法,不要使用集合自身的remove方法,否则报错,ConcurrentModificationException 修改结构异常。

②每次集合结构改变时,都必须重写迭代器。去更新和对应上集合的新结构,否则就会报异常

package Collection集合.Collection集合父接口;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
 * 集合自身remove方法和迭代器的remove方法的区别?
 *1、提前:迭代器就是copy了一个一模一样结构的额外的集合的缓存区(专门用来遍历的),
 * 暂时把集合的元素放进去迭代器里面去遍历输出,迭代器遍历一个就放回去集合一个。
 * 2、因此集合自身结构发生变动时(增删),对应的迭代器也要变动去重新获取。
 * 不会自己更新的。不然就会ConcurrentModificationException异常报错。
 *
 * 3、区别1、通过用集合自身的c.remove(有参)方法,删除集合自身的特指元素,(不会更新迭代器)
 * 和(不会同步去删除迭代器所缓存的元素的)。 导致集合结构已经改变但是已经对应不上迭代器里面结构,不可再遍历的时候使用。
 *
 *4、区别2、通过迭代器自身的i.remove(无参)方法,删除迭代器遍历时所缓存的当前元素,
 * 是会“自动更新迭代器”和“同步去删除对应的集合自身的元素”。迭代器结构是可以对应上集合结构的,可以在遍历的时候使用。
 * 5、具体区别看下面程序详解
 */

//第6点详解,了解迭代器
public class 集合自身和迭代器的remove方法区别 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add(1);
        c.add(2);
        c.add(3);
        c.add(4);

        Iterator i = c.iterator();//迭代器就像是一个额外的集合缓存区,暂时把元素放进去迭代器里面去遍历输出,迭代器迭代一个就放回去集合一个。
                                  // 因此集合机构发生变动时,对应的迭代器也要变动去重新获取。不会自己更新的。不然就会报错
        while (i.hasNext()){
            Object o = i.next();
           // c.remove(o); //报错,ConcurrentModificationException 修改结构异常。
                        //注意,这里调用的是集合本身的c.remove(o);删除元素方法,
                       //说明 每次循环删除的是集合里面的元素,而不是迭代器里面缓存的元素。此时集合结构已经改变但是已经对应不上迭代器里面结构,因为迭代器不会同步删。
                      //总结 ①用集合自身的c.remove方法,删除集合自身的元素,是不会同步去自动删除迭代器缓存的元素的。
                     //     ②因此每次集合结构改变时,都必须重写迭代器。去更新和对应上集合的新结构,否则就会报异常


            i.remove();  // 不会报错,
                        // 注意,这里调用的是迭代器本身的i.remove(o);删除方法,删除迭代器所指向的当前元素。
                       // 因为每次循环删除的都是迭代器所指向的当前的缓存的元素。
                      // 总结: 用迭代器自身的i.remove方法,删除迭代器缓存的元素,是会自动同步删除对应的集合自身的元素结构。

            System.out.println(o);//1234
        }
        System.out.println(c.size()); //因为元素从集合中删除1234,因此此时集合大小长度为0.空的
    }
}

三、List子接口详解:

一、List简介:
①List就是列表的意思
②当我们不知道存储的数据有多少的情况,我们就可以使用List 接口来完成存储数据的工作因为List最大的特点就是:
通过实现类例如Arraylist的构造方法,能够自动根据插入数量来动态改变容器的大小。(即自己选择数组容量)
③List集合代表一个有序可重复的集合,集合中的每一个元素都有其对应的顺序索引。List集合允许使用重复的元素,可以通过索引访问指定位置的集合元素

④因为含有顺序索引,所以第一次添加元素的索引为0,第二次添加索引为1,依次…

二、List子接口特有常用方法:
特点:有序可重复
由于有了下标,方法和Collection有不同之处,也多出了新的遍历方式
(注意这是List子接口特有的,Collection父接口没有)

1、增、l.add(下标,元素): 添加元素到指定的下标盒子
2、删、l.remove(下标); 删除指定下标的元素。 (List的remove方法中途删会影响后面的元素下标(系统会重新自动排序)。即后面的元素下标就会往前进一位,即下标1会进一位更新为下标0。)
3、查、l.get(下标): 获取指定下标的元素 【l.get(i); 相当于数组的array[i]获得具体元素】 +【 l.size();相当于数组的arg.length获得长度】
4、改:l.set(下标,改后的元素): 修改元素到指定的下标盒子
5、l.indexOf(元素): 获取元素的下标
注意:其他的父接口Collection的方法,List子接口也会有,只是有些方法参数都要改用下标了。(add两种,size不变)
比如l.remove(下标); 移除方法要用下标代替具体值去删。

三、疑问来了,Collection的add添加方法和List特有的add添加方法有什么区别?
(注意List可以同时拥有两个add方法)
1、储存元素的流程不同,对于ArrayList数组类来说:
Collection的add方法是无下标的,采用尾插法,直接往数组末端一个一个去添加具体元素。(永远在数组的最后一个数字的后面一个一个放,添加效率更高,)
而List特有的add方法是有下标,通过下标 放具体元素到特定位置。(还要指定盒子去放,添加效率慢)
因此实际开发,用无下标的add方法较多,效率高。

2、因此对于List当中,无下标的add方法(尾插法)比有下标的add方法效率更高。
对于ArrayList数组来说的共同优缺点都是;把检索发挥到极致【因为要保证内存地址连续,不便于删除和插入(有下标很快的找到和修改,插入删除还要移位)】
对于LinkedList链表来说的共同优缺点都是;把随机增删发挥到极致【因为内存地址不连续,查找检索效率慢,只能头节点慢慢轮流找】

四、List子接口所有的遍历方式。(3种)、(List子接口在原来Collection父接口基础上,可以用for循环以数组形式遍历集合)
因为有List特殊方法,有下标可以获得具体元素。(注意LinkedList无下标不推荐用for,ArrayList和Vector可以)
1、for循环 注意;【l.get(i); 相当于数组的array[i]获得具体元素】 +【 l.size();相当于数组的arg.length获得长度】
2、迭代器Iterator+while (原来的也能用)
3、foreach (原来的也能用)

package Collection集合.List集合子接口;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public class List集合简介和特有方法 {
    public static void main(String[] args) {

        List<Integer> l=new ArrayList<>();
        l.add(0,1); //1自动装箱为Integer
        l.add(1,2);//往数组特定下标盒子添加元素,查询更快
        l.add(3);//直接往数组的末尾添加元素,效率更高

//        l.remove(0);  (List的remove方法中途删会影响后面的元素下标(系统会重新自动排序)。即后面的元素下标就会往前进一位,即下标1会进一位更新为下标0。)

        Integer i=l.get(0);  //查找   查找下标为0的元素。
        System.out.println(i);//1

        l.set(0,3);//改  把下标为0的元素改为3
        System.out.println(l.get(0));//3   0下标的值由原来的1改为了3

        int i1=l.indexOf(2);//获取某个元素的下标。 2的元素下标为1
        System.out.println(i1);//1

        l.remove(2);
        System.out.println(l.size());//2。  原来有3个元素,现在变成2个

        doSome();//
        doIterator();
        doForeach();

    }

    //一、List接口新的for循环遍历方式。  注意;【l.get(i); 相当于数组的array[i]获得具体元素】 +【 l.size();相当于数组的arg.length获得长度】
    public static void doSome(){
        List<Integer> l=new ArrayList<>();
        l.add(0,1); //下标0,第一个数
        l.add(1,2);  //代表1,第二个数
        for(int i=0;i<l.size();i++){
            System.out.println(l.get(i));//1 2
        }
    }
    //二、原来的iterator迭代器遍历方法也行。
    public static void doIterator(){
        List c = new ArrayList(); //多态间接new对象。(List extends  Collection --->List l = ArrayList();)。
        //添加元素
        c.add(100);//这里100已经自动装箱为包装类,因此也是一个类对象。
        c.add("abc");//String字符串也是一个对象
        c.add("123");
        c.add(new Object());

        Iterator i = c.iterator();      //拿迭代器对象。
        while(i.hasNext()){           //判断是否有下一个元素  。  (如果放true,一直取元素会报异常NoSuchElementException)
            Object o = i.next();     //取出该一个元素。
            System.out.println(o);
        }
    }
 //三、原来的foreach方法也可以遍历
    public  static  void doForeach(){
        List l = new ArrayList();
        l.add(1);
        l.add(2);
        l.add(3);
        for (Object o : l){
            System.out.println(o);
        }
    }
    
}

A、深入List子接口的ArrayList类:

1、ArrayList基本概念:
①ArrayList类:是非线程安全的,底层采用了数组的数据结构。(默认是一个Object数组)

transient Object[] elementData;

ArrayList类的一个参数构造方法,是指定底层数组的初始化容量的,但本身无参构造给底层数组的默认初始化容量是10(在add添加第一个元素时,就初始化10)

③如果一定要扩容(add)的话,扩容之后的容量是:是原来容量的1.5倍(相当于增加了0.5倍/二分之一),在源代码中就是使用到位运算符右>>1去实现的。

2、番外:二进制位运算符binary: 1 2 4 8 16 32 64 128
(忘了就去看文章二进制位运算符,x的二进制往右y位,则x/2的y次方。 x的二进制往左y位,则x2的y次方)*

ArrayList底层是使用的 二进制位运算符往右一位:>>1(即除2一次方)

① 假设原容量为10,则扩大了=10/2一次方=5, 则扩容后的容量为:10+5= 15 即是原来容量的1.5倍
②假设原容量为100,则扩大了=100/2一次方=50,则扩容后的容量为:100+50= 150 即也是原来容量的1.5倍

3、ArrayList类的优缺点:
(因为这底层是数组结构,优缺点和数组一样,查询快,增删慢)
优点:检索快(因为索引的内存地址是连续的)
缺点:增删慢(因为添加元素就是,新建一个新的大的数组,然后复制过去,比较麻烦)
因此,由于其扩容效率低的问题,因此要少点扩容,所以一般要预估和指定好ArrayList的初始容量。这是优化ArrayList的最好策略

4、普通数组的初始化数组容量和ArrayList类的底层数组和的区别?
①一个是单纯的数组可以直接通过“动静态初始化”数组容量,
int[] a2 = new int[10]; 动态初始化10个容量 ,默认值为10个0。

②一个是数组类外面套了一个类,即ArrayList类的底层是数组类,数组是作为一个实例属性嵌套进ArrayList类中,因此要通过其中一个构造方法给数组容量)
学会看懂和理解源代码,不要求你会写,这很重要important!!!

ArrayList类的构造方法的源代码,底层有一个数组作为ArrayList类的实例属性

transient Object[] elementData;
//有参给数组赋值指定initialCapacity初始化容量
 public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
//无参给数组赋值默认的初始化容量:10
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
//有参构造:将Set集合转换为List集合(集合Collection的toArray中实现)
  public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

类似于StringBuffer类底层是byte[]数组,默认容量16但会自动扩容)是字符串拼接的工具类;(相当于是一个字符串的缓冲区,也叫,长度容量为16的byte[]数组,就是把字符串放到缓存区里面拼接。)

5、通过上述源代码知道,ArrayList有三个构造方法:
1、无参构造 List list1 = new ArrayList(); //底层Object数组默认初始化容量为10
2、有参构造 List list2 = new ArrayList(100); //底层Object数组指定初始化容量为100
3、有参构造 List list3 = new ArrayList(new HashSet()); //特殊:同等级下,强制将Set集合转换为List集合

比较普通数组的length属性和arraylist的size方法区别?
(主要体现在动态初始化中)
size方法只是查看当前集合的元素个数,不是看元素的初始化容量)
因为底层数组他没有设置length属性,因此只有size方法是但只是用于查看集合里面的元素个数,因此不是代表底层Object数组的动态初始化容量!!!


package Collection集合.List集合子接口;
import java.util.ArrayList;
import java.util.List;

public class List子接口的ArrayList类 {
    public static void main(String[] args) {
        int[] array = new int[10];
        System.out.println(array.length); // 10  length属性就是查看当前数组的长度(即个数和初始化容量)
        String[] array2  = {"abc","def","ghi"};
        System.out.println(array2[0].length());//    length方法是看字符串类的长度

        List list1 = new ArrayList();   //底层数组默认初始化容量为10
        System.out.println(list1.size());//size只是查看集合里面有几个元素  0

        List list2 = new ArrayList(100);//底层数组指定初始化容量为100
        System.out.println(list2.size());//size只是查看集合里面有几个元素  0

    }
}

B、深入List子接口的Vector类:

一、Vector类:
①底层也是一个数组结构,初始化容量也是10
②如要扩容,那么扩容之后的容量是:是原来容量的2倍(死记即可)
和ArrayList相反,该Vector类是线程安全的,效率比较低,用的比较少,因为现在有了更好的控制线程安全的方法就是:

(直接非线程安全直接转换为线程安全的即可,快狠准!) 
 Collections.synchronizedList(list)
后面具体讲 Collections这个工具类!

④ 总结:因此实际开发大多用ArrayList是非线程安全代替Vector类,效率高

了解以下代码即可:

package Collection集合.List集合子接口;

import java.util.*;
public class List子接口的Vector类{
    public static void main(String[] args) {
//        初始化容量也是10,如要扩容,扩容之后的容量是:是原来容量的2倍(死记)
        Vector vector1 = new Vector();  // List vector1 = new Vector();也行
        vector1.add(1);
        vector1.add(2);
        vector1.add(3);
        vector1.add(4);
        vector1.add(5);
        vector1.add(6);
        vector1.add(7);
        vector1.add(8);
        vector1.add(9);
        vector1.add(10);
        vector1.add(11); //扩容:10--->20---->40
        Iterator i = vector1.iterator();
        while (i.hasNext()){
            Object o = i.next();
            System.out.println(o);
        }


        List list = new ArrayList();//非线程安全的
        Collections.synchronizedList(list);// 转换为线程安全的

        list.add("123"); //现在这个程序就是线程安全的了
        list.add("abc");
        list.add("rng");
        list.add("edg");


    }
}

C、深入List子接口的LinkedList类:

1、LinkedList双链表概念:
Node节点是单/双链表的基本单位
(即LinkedList双链表类是由一个个节点类组成。【即一个链表Link类套一个node节点类,节点类中又套下一个节点类】)。
①LinkedList源代码底层是一个双向链表数据结构,它和ArrayList(10)和Vector(10)不同, 没有初始化容量

2、单链表数据结构学习:

单向链表每一个节点盒子都对应两个属性:
① 当前存储的引用元素,
②next指向下一个节点的内存地址。(然后一直套娃)
【其实node节点盒子一开始放元素数据和一个null地址的,直到下一个node节点对象new出来,才将新地址放到上一个节点的null位置,去指向下一个node节点】

单链表的数据内存图和增删内存图:
在这里插入图片描述

3、双链表数据结构学习(即LinkedList底层代码):

双向链表每一个节点也都有对应三个属性:
① 存储的当前元素
②pre指向上一个节点的内存地址
③next指向下一个节点的内存地址。
由于指向上一个或下一个节点的特点:
【地址是双向的,即两端节点都可查到中间节点的地址,First头端的next下一个节点地址属性指向中间节点地址, 同时Last尾端的prev上一个节点地址属性也指向中间节点地址。】

在这里插入图片描述

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

3、链表和数组优缺点比较;????(和数组正好相反)
【1】、双链表优缺点?(适合需要增删的业务)
①优点:增删快(因为添加元素,就是添加节点,只需要在节点两端上加两个引用)
② 缺点:查询慢(虽然也有下标,但是地址不连续,有下标也没用,顺序是全乱的。因此必须从头节点开始查询next下一个节点,经过一个又一个节点遍历,最终找到)
因为节点地址不连续,因此链表的查询效率很慢,只能通过头node节点轮流一个一个查

【2】、数组优缺点?(适合需要检索的业务)
① 优点:查询快
②缺点:增删慢
因为要保证内存地址连续,所以删除或者增加内部某个元素都会发生较大的变动位移(要把后面元素往前挪或者往后移),
因此增删的效率较低。但是删除和增加最后一个元素没影响。

4、LinkedList链表的遍历方式?(代码例子看上面的List集合)
①for循环 注意;【l.get(i); 相当于数组的array[i]获得具体元素】 +【 l.size();类似数组的arg.length获得长度】
②迭代器Iterator+while (原来的也能用)
③foreach (原来的也能用)
注意:链表不推荐使用for循环遍历,推荐迭代器和foreach,foreach底层也是iterator迭代器。

5、面试题:学会自己自定义设计一个单链表数据结构:

package Collection集合.List集合子接口;

import java.util.LinkedList;
import java.util.List;

public class List子接口的LinkedList类 {
    public static void main(String[] args) {
        //学会自己单独实现一个单链表程序,面试数据算法题!
        //在下面的程序中自定义实现一个单链表数据结构.(一个链表类套一个节点类,节点类中又套下一个节点类)
        //理解一下就好,实际sun公司已经写好了LinkedList类了给我们使用。
        Link link1 = new Link();//这里为自定义的一个链表类结构Link自定义类。
        link1.add("abc"); //自己写的add方法
        link1.add("def");
        link1.add("ghi");
        System.out.println(link1.getSize());//3   自己实现的getSize方法,获得Link集合中元素个数


        //sun写好的LinkedList集合
        List link = new LinkedList(); // 这里为sun公司已经写好了LinkedList类
        link.add('a');    //sun写好的add方法
        link.add('b');
        link.add('c');
        System.out.println(link.size()); //3     sun写好的size方法,获得List集合中元素个数



    }
}

//在程序中自己去实现一个单链表数据结构.(一个链表类套一个节点类,节点类中又套下一个节点类)
class Link{
    //实例变量
    Node First= null; //实例变量 链表的First头节点,默认值就是null ,每一个节点包含(一个是存储的引用元素,一个是指向下一个节点的内存地址)

    int size = 0; //不赋值,默认值也为0

     public int getSize(){
        return size;
    }

    public void add(Object data){   //向链表中提供增删查改实现方法
        //添加元素,即创建节点对象
        //让之前单链表的末尾节点去next指向新的节点对象
        //有可能这个元素是第一个、第二个、或者第三个
        if (First==null){
            //说明还没有头节点,要创建 new 一个头结点。
            //这时候的节点,既是一个头节点,也是一个末尾节点去指向下一个节点
            //当成末尾节点的话,就可以去new下一个Node对象赋值。
            First = new Node(data,null);//再去有参构造,给节点内部属性赋值
        }else {

            //反之,说明头节点不是空,已经有头节点对象了
            //因此这时需要找到CurrentLastNode末尾节点,让当前末尾节点去再next指向下一个新的节点对象
            //通过寻找方法去找末尾节点,再return返回CurrentLastNode。
            Node CurrentLastNode = findLast(First);
            CurrentLastNode.next = new Node(data,null);
            
        }
        size++; //if、else执行之后一定会输出一个Node节点即集合元素,那么size就加一个元素。
    }

    public void remove(Object data){  //删

    }
    public void find(Object data){   //查

    }

    public void modify(Object data){   //改

    }

    /**
     * //专门的方法,寻找查找末尾节点的方法
     * @param node
     * @return null
     */
    private Node findLast(Node node) {  //专门的方法,寻找查找末尾节点的方法
        if ( node.next == null){
            //如果一个节点的next为空null,说明这个节点是末尾节点,
            //或者说是后面没有下一个节点的节点,那么就可以返回去他后面新建下一个节点。
            return node;
        }else {
            //程序如果能到这里:说明这个节点不是末尾节点,再去递归下一个,直到下一个节点的内存地址即next为null说明就是末尾节点了。
            //这里运用了递归算法!!!!
            return findLast(node.next);
        }

    }
}


class Node{
    Object elements;//一个是存储的引用元素
    Node next;//一个是指向下一个节点的内存地址


    public Node(){  //无参构造

    }
    public Node(Object elements, Node next) {  //有参构造
        this.elements = elements;
        this.next = next;
    }


}

四、Set子接口详解:

1、Set接口,总特点:
放无序不可重复元素,元素没有下标,无特有方法。

2、Set接口中有2个常用的子类:
(注意:有关Set集合所存所有的元素,底层都会存到对应的Map集合的Key盒子中去。)

[1]HashSet类:
①实际上底层new了一个HashMap类(即实际是存到HashMap集合中的,底层采用了哈希表)。
② 特点:放无序不可重复元素.不会排序

[2]TreeSet类:
①实际上底层new了一个TreeMap类(即实际是存到TreeMap集合中的,底层采用了二叉树数据结构)。
②由于它的上面还有一个自己父接口:SortedSet接口,因此它有多一个特点:会从小到大自动排序。
③特点:放无序不可重复元素,但是元素会自动排序。
(PS:这里的无序和排序不同,无序是指无下标,存和取出来的元素顺序不同。排序是指从小到大排序。)

3、复习Set注意事项:
【1】有关Set集合全部所存的东西,底层实际都会存到对应的Map集合的key部分去。
【2】TreeSet由于实现了另一个父接口SortedSet接口:可以从小到大自动排序
因此有关二叉树的类都是会实现两个父接口。(因此二叉树类相比别的数据结构类,二叉树类会多出一个自动排序功能)
【3】Set和Map区别:
①虽然Set集合所存的数据,底层都会把它们存去Map集合中的key盒子中,但是两者还是有区别的。
②Set集合和Map集合区别:Set只能单个存元素,Map以键值对(key-value)存元素,但共同特点都是:存无序不可重复的元素,无下标。

4、HashSet类代码展示:

//HashSet等于HashMap直接去看HashMap即可
//Set和Map区别:Set存一个元素(相当于key),Map存两个元素(key-value)。都是存无序不可重复的元素,无下标。
package Collection集合.Set集合子接口;

import java.util.HashSet;
import java.util.Set;

public class Set子接口的HashSet类 {
    public static void main(String[] args) {
        //Set接口,特点:放无序不可重复元素,元素没有下标
        Set<String> s = new HashSet<>();
        s.add("1");
        s.add("1");
        s.add("3");
        s.add("2");
        s.add("3");
        s.add("3");
        s.add("4");
      //遍历HashSet集合
        for (String s1: s) {
            System.out.println(s1);//无序不可重复,因此结果为1 2 3 4
        }
        
    }
}

5、详解TreeSet类:
【1】底层是新建了一个TreeMap,因此TreeSet实际存在TreeMap的key部分,二叉树的数据结构是左小右大的数据结构,因此TreeSet属于中序遍历,它能够自动排序。
【2】特点:放无序不可重复元素,但是元素会自动排序,也叫可排序集合
【3】 注意:(重点!如果是存放的是自定义的对象类,那么就要去必须重写自定义比较方法,才能去自动排序,否则报错)。
Exception in thread “main” java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable

【4】具体自定类怎么重写?
第1种、 直接在原来的自定义类上实现lang包下的Comparable接口并且在里面比较(即重写实现compareTo方法。)。
【class Student implements Comparable< Student >】
(这个自定义类实际就是变为了Comparable接口,给自定义类提供了可以比较的功能,直接在自定义类中比较即可)


第2种、 原来的自定义类不用动,额外重新定义一个比较类,在这个类去实现了util包下自带的Comparator比较器接口并且在里面比较(即重写compare方法)
再将这个比较类对象封装为TreeSet的一个对象属性,即可以放到TreeSet构造方法参数中。
【class CompareToStudent implements Comparator< Student1 > 】
(这个比较类实际就是充当了比较器Comparator,不用动原自定义类,就把比较器(即新定义的比较类)放入集合构造方法中,即用比较器去比较)

第3种、可以将第二种方式中的,额外新定义的比较器类,去升级变为匿名内部类。(但是可读性变差,变为一次性比较,不推荐使用)

一、先了解底层源代码二叉树实现原理:
 public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        
        这里开始看comparator and comparable实现原理:
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {     //如果构造器不为null,这种是TreeMap有参构造
            do {
                parent = t;
                cmp = cpr.compare(key, t.key); 
                //则把当前key和已经存进去t.key放入比较方法中,返回int
                if (cmp < 0)   //比较结果要是小于0,说明key比key1小,
                    t = t.left; //就会把他作为左子数即较小数存到左边
                else if (cmp > 0)  
                    t = t.right; //反之作为右子数存到右边
                else
                    return t.setValue(value);
            } while (t != null); //直到没有元素结束
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            //如果为TreeMap构造为null,即是无参构造,构造器为null。
            //实现Comparable的compareTo方法
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    
二、跟着我一起看TreeMap也是TreeSet构造方法源代码:
public TreeMap() {           //无参,无构造器
        comparator = null;
    }
    
public TreeMap(Comparator<? super K> comparator) {  //有参,用构造器
        this.comparator = comparator;
    }
    
三、看完源代码来分析下面第一种自定义比较方法?
1、如果一开始是调无参构造方法,则TreeMap/TreeSet集合的无参构造方法会有一个比较器comparator默认为null空,
当put方法执行时,会进入一个ifelse语句,if先会看比较器comparator是否为null,
如果比较器是null,就会反之执行else就会将自定义Student类强转为Comparable,但是当前程序的Student和Comparable是没有任何关系的,
因此就会出现强转失败异常。ClassCastException异常:(没有实现接口去实现比较方法)

2、解决方法:
①去解决else分支:即当前集合的比较器comparator是null时的情况(即调用无参构造方法)
②就要去在自定义类Student中去实现Comparable接口,使他们变成继承关系就能去强转了。Student强转Comparable转成功后,就会去自动去调用和实现Comparable接口的compareTo方法在里面进行比较
实现Comparable接口(【class Student implements Comparable<Student>】)

③compareTo方法里面是一个dowhile循环,拿当前存进去的k1对象和已经存进去的k2.k3对象比

④重写compareTo方法(具体规则如下):
idea重写Comparable接口的compareTo方法只会重写一个外壳给你,
具体怎么比较的规则,要你自己去编写。
编写规则如下:
[1]自定义类如果对象属性为字符串引用类,则字符串之间比较只能用String自带的compareTo方法, 
(不要把Comparable接口的compareTo和String的compareTo弄混了)
死记规则:return this.name.compareTo(student.name); + 重写toString方法
this.name>student.name 小于则返回负数 ,等于返回0,大于返回正数。

[2]如果自定义类属性为基本类可以,则可以直接相减比较大小
死记规则:return this.age - student.age;+ 重写自定义类toString方法
小于则返回负数 ,等于返回0,大于返回正数。


四、第二种自定义比较方法详解?(在创建Tree类集合对象的构造方法放比较器的方式)?(适合随时修改的比较器)

①即一开始执行if分支:即TreeMap/TreeSet集合的构造方法中的比较器comparator一开始不是一个null,是有比较器的comparator。
②流程:原来的自定义类不用动,额外重新定义一个比较类,比较类去实现了Comparator接口(这个类实际就是充当了比较器Comparator),
并且重写实现Comparator接口的compare方法在里面比。
最后再将这个比较类对象放到TreeMap构造方法参数中。
class CompareToStudent implements Comparator<Student1>
Map<Student1,Integer> m=new TreeMap<>(new CompareToStudent());

③重写compare方法的比较规则和Comparable接口的compareTo方法一样。
编写规则如下:
[1]自定义类如果对象属性为字符串引用类,则字符串之间比较只能用String自带的compareTo方法, 
(不要把Comparable接口的compareTo和String的compareTo弄混了)
死记规则:
return this.name.compareTo(student.name); + 重写toString方法
this.name>student.name 
(小于则返回负数 ,等于返回0,大于返回正数)

[2]如果自定义类属性为基本类可以,则可以直接相减比较大小
死记规则:return this.age - student.age;+ 重写自定义类toString方法
小于则返回负数 ,等于返回0,大于返回正数。


五、总结:
   TreeSet/TreeMap的第一种排序方式是实现Comparable接口覆写compareTo方法比较,(自然排序)
   TreeSet/TreeMap的第二种排序方式是实现Comparator接口覆写compare方法比较,(比较器comparator排序)
   第二种Comparator比较器排序是在集合一初始化就拥有的,所以要讲此接口的实例对象传入到TreeSet/TreeMap的构造函数中!!!

六:两种自定义排序方法怎么选?????????????????
1、(Comparable适合固定(升序或降序规则)或者 单一的较规则的比较方法):因为只能在自定义对象类中实现一次Comparable接口,就不可变了
2、(Comparator适合随时修改任何类(已写好类或者自定义类)的(升序或降序比较规则)或者  多种比较规则的比较方法):        
  因为是自己额外创建另外一个比较类去实现Comparator接口
  因此可以创建多个比较类去同时实现Comparator接口,这样用哪个比较类的比较规则,就去放哪个比较类的对象放进去集合的构造方法里面。
3、例如 可以用比较类1new CompareToStudent1() 。就放Map<Student1,Integer> m=new TreeMap<>(new CompareToStudent1());
       也可以用比较类2new CompareToStudent2()。就放Map<Student1,Integer> m=new TreeMap<>(new CompareToStudent2());

展示TreeSet内置类如何自动排序和自定义类手动排序::
这里使用的是第一种方法:实现Comparable接口

package Collection集合.Set集合子接口;

import java.util.Set;
import java.util.TreeSet;

public class Set子接口的TreeSet类 {
    public static void main(String[] args) {
        //存放的是String类是系统写好的类,不用重写自定义比较方法
        TreeSet<String> treeset = new  TreeSet();
        treeset.add("lisi");
        treeset.add("zhangsan");
        treeset.add("wangwu");
        treeset.add("jacky");
        for (String s1 : treeset) {
            System.out.print(" "+s1); // jacky lisi wangwu zhangsan  ,按照字典的头字母,升降顺序自动排序
        }
        System.out.println();


       //存放的是Integer类也是系统写好的,不用重写自定义比较方法
        TreeSet<Integer> treeset1 = new  TreeSet();
        treeset1.add(123);
        treeset1.add(124);
        treeset1.add(125);
        treeset1.add(126);
        treeset1.add(127);
        for (Integer i : treeset1){
            System.out.print(" "+i); //123 124 125 126 127 按照数字大小升序自动排序
        }
        System.out.println();



       //存放的是自己定义的Student类,那么就必须要重写自定义比较方法,才能去实现自动排序。
        Set<Student> m = new TreeSet();
        m.add(new Student("zhang",18));
        m.add(new Student("lisi",19));
        m.add(new Student("wangwu",20));
        for(Object o:m){
            System.out.print(" "+ o);//自定义类记得重写toString方法,lisi wangwu zhang    虽然也是无序(即存进去和取的出来的顺序不一样),但会按字典头字母,从小到大排序
        }
        System.out.println();
    }
}




//TreeSet放自定义类对象是怎么实现自定义排序方法。
// 解决方法:就要在自定义类Student中去实现Comparable接口,并且一并重写实现接口中的 compareTo方法,equals可以不写,toString记得要写。
class Student implements Comparable<Student>{
    int age ;
    String name;
    public Student(String name,int age) {

        this.name = name;
        this.age = age;
    }


    //idea重写compareTo 只会重写一个外壳给你,具体怎么比较的规则,要你自己去编写。(拿当前存进去的k对象和已经存进去的k对象比)
    @Override
    public int compareTo(Student student){ //传参数进去比较 Student student = new Student("zhang")。参数传进去然后就能用类里面的属性比较
        //s1.compareTo(s2);
        //this 是  s1  当前存进去的对象
        //student 是 s2  已经存进去的
        //s1 和 s2比较的时候就是 引用this 和student比较。(这里用的是二叉树算法,拿当前存进去的k对象和已经存进去的k对象比)

      /*  String name1 = this.name;
        String name2 = student.name;
        if (name1 == name2){
            return 0;
        }else if (name1> name2){
            return 1;
        }else {
            return -1;
        }*/
      //简化版
      // 比较规则:这里规则是先比名字,再去比年龄。
        if (this.age == student.age){
            return this.name.compareTo(student.name);

        }else {  //年龄是int可以直接比
            return this.age - student.age;
            //注意:自定义类如果对象属性为字符串引用类,则字符串之间比较只能用String自带的compareTo方法,返回int
            // 死记规则:return this.name.compareTo(student.name);
            //this.name>student.name 小于则返回负数 ,等于返回0,大于返回正数。
            //如果自定义类属性为基本类可以,则可以直接相减比较大小
            //死记规则:return this.age - student.age;
            //小于则返回负数 ,等于返回0,大于返回正数。
        }
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

//了解String自带的compareTo方法,
 /*  ② " ".compareTo("abc");方法         通过对比两个字符串大小,(字符串比较不能用<>=)  并返回int类型。
        int c1="abv".compareTo("abc"); (注意;大于0说明左边比右边大,小于0右比左大,等于0一样大。)
       compareTo方法和equals方法区别,一个不仅可以比较是否相等还能比较字符串大小(返回int),一个比较对象地址里具体内容是否相等(返回boolean)。
  */

展示TreeMap内置类如何自动排序和自定义类手动排序:
这里使用的是第二种方法:实现Comparator接口

2、TreeMap:
TreeMap的底层是一个二叉树,能够自动对K部分进行从小到大排序。
也需要定义一个比较器。
3、下面是自定义比较的第二种方式。
*/
package Map集合;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class aTreeMap{
    public static void main(String[] args) {

    /*Map<Student1,Integer> m=new TreeMap<>(new Comparator<Student1>() {  //引出匿名内部类,在实际参数里面定义,new+ 接口名+()+{}的方法,去定义接口的方法。
            @Override                                                     //但是这种方法不如直接新建一个类,匿名内部类可读性差。
            public int compare(Student1 o1, Student1 o2) {
                return o1.name.compareTo(o2.name);
            }
        });*/

        //map一定是放key-value 两个元素的。
        //泛型指定:<Student1,Integer>

//        Comparator c = new CompareToStudent();
        Map<Student1,Integer> m = new TreeMap<>(new CompareToStudent());
        //放new CompareToStudent()   需要无参构造放一个比较类对象实现自带的比较器方法,
        //相当于Comparator c = new CompareToStudent()
        m.put(new Student1("zhangsan",11),1);  //Comparator肯定在TreeMap类被属性实例化了,
        //如下图,在TreeMap某个方法里面再去调comparator属性的方法,等于多态去调实现类的compare方法
        m.put(new Student1("lisi",12),2);   //CompareToStudent like a 充当了 Comparator 去比较学生大小排序 .
        m.put(new Student1("wangwu",20),3);//这个多态Comparator实现方法只有在TreeMap构造方法放了实现类对象才会去执行,不放就会执行另一个构造方法。
        /*
          public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
         public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        */

        Set set=m.entrySet();
        for(Object s:set){
            System.out.print(" "+s);
            //这里可以使用倒序输出:Student1{age=20, name='wangwu'}=3 Student1{age=12, name='lisi'}=2 
            //Student1{age=11, name='zhangsan'}=1
            //比较器比较方法优点,Comparator适合随时修改(升序或降序规则)或者多种比较规则的比较方法):因为是自己额外创建另外一个比较类去实现Comparator接口
        }
        System.out.println();
    }
}


class Student1{  //原来的自动类不需要变动
    int age;
    String name;
    public Student1(String name,int age) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student1{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

//额外重新定义一个比较类,在这个类去实现了Comparator接口
//(这个类实际就是充当了比较器Comparator), Comparator<Student1> c = new 
//CompareToStudent() 再将这个比较类放到TreeSet构造方法参数中
class CompareToStudent implements Comparator<Student1> {

    @Override
    public int compare(Student1 s1, Student1 s2) {   //这里不能用this.因为出了当前对象的自定义类的作用域。因此直接传两个参数去比较
//这里比较规则是,先比年龄比,年龄一样就去再去比name
        if (s1.age != s2.age){
              return  s1.age - s2.age;
        }else{
            return s1.name.compareTo(s2.name);
        }
    }
}


五、Map集合接口详解:

1、Map集合基本概念:
①Map接口存储数据是按Key-Value键值对(一对一对存)的方式进行存储的。(key类似下标获取元素,只是这个key下标可以是任何类型,也是去一一对应value)
②键值对包括key和value两个部分。(key和value都是引用类型,存的都是对象内存地址)。
③根据指定的key元素对象,去 Map集合获取相应的value对象。 如果Map集合中没有指定的key元素存在,则返回null。
④由于是存的键值对key-value,因此Map集合的泛型也得是<T,E>指定两个泛型

2、Map集合特点:
①Map集合可以一次性存储两个对象;
②无序不可重复,key不可重,但value可重,且一一对应
③Map集合中的key必须保证唯一性(存储的key元素不能重复,value可以重复,但是一个key只能对应一个value);

3、Map接口中,有3个常用的子类:
(1)HashMap:非线程安全的,底层是哈希表数据结构
(2)TreeMap:底层是二叉树数据结构,它的上面还有一个父接口:SortedMap,可以自动排序
(3)HashTable:线程安全的,底层是哈希表数据结构(很少用了)
(4)注意:HashTable中有一个子类Properties属性类,只能是以key-value键值对儿存储“String字符串”
Properties特别重要充当属性文件去IO读取,后面IO和反射会用到

4、抽象了解Collection和Map集合区别?
①Collection接口下的所有集合中保存的对象都是孤立的。对象和对象之间并没有任何关系存在。在生活中对象和对象之间必然会一定的联系存在。而我们学习的Collection接口下的所有子接口或者实现类(集合)它们中的确可以保存对象,但是没有办法维护这些对象之间的关系。
②而学习的Map集合,它也是存放对象的,但是可以对其中的对象进行关系的维护,因而把Collection集合称为单列集合。Map被称为双列集合
③细致区别:
1)Map中键唯一,值没有要求。Collection中只有Set体系要求元素唯一(Set只能放无序不可重复元素)
2)Map的数据结构针对键而言,Collection的数据结构针对元素而言。
3)Map是双列集合顶级接口,Collection是单列集合顶级接口,他们两个分别是两种集合的顶级爸爸接口,之间没有必然的联系。

5、Map集合接口常用的方法:

1、put(key,value):向集合中添加元素 类似List的add方法
2、clear():清除集合中的元素
3、get(key):获取集合中key键的value值 和list的get去获取下标对应的值差不多
4、remove(key):移除某个key和其对应的value (key、value一起删,两者一一对应的)
5、containsKey():集合中是否有key值 记得如果是自定义的类,一定要重写equals方法和toString,才能去比较对象具体内容,而不是地址
6、containsValue():集合中是否有value值
7、size():Map集合中键值对的数量。
8、isEmpty:集合中元素是否为空
9、keySet():获取集合中所有key键,并返回值是一个set集合类型(实际Set集合底层都是存在map集合的key盒子中,而key盒子中都是Set集合类型)
10、values():获取集合中所有的value值,并返回值一个Collection集合类型(value盒子中都是Collection集合类型)
11、超级特殊方法:
entrySet()方法:将一个Map(键值对融合一起1=123)转换成一个Set集合,返回也是一个Set类型中的泛型<Map.Entry<K,V>>。
注意1:这里返回的实际是泛型<Map.Entry<K,V>>类型(这是静态内部类(类套娃类),要用类名.类名的方式调)。
例如Set<Map.Entry<Integer,String>> set = new HashSet();
一个大Set集合去泛型指定一个(Map.Entry静态内部类),即Map集合里有个Entry静态内部类,然后Entry静态内部类再去套一个小泛型<Integer,String>。ps:entry也属于Map集合因此泛型要键值对形式
注意2:List list3 = new ArrayList(new HashSet());------>ArrayList有参构造方法,将一个Set集合转换为一个List集合

6、Map集合遍历方法???
Map集合没有自己的迭代器Iterator,只能间接遍历。

第一、效率低(少用,要多写代码)
使用keySet():方法获取Map集合中所有key键,返回一个set集合类型,因为key盒子中都是Set集合类型,
然后再去遍历Set集合就等于间接遍历Map集合。(注意:遍历过程中,要用get(key)把每个key键对应的value值一起取出这样才完整遍历完Map集合)

第二、效率高(适用于大量的数据)
Set<Map.Entry<Integer,String>> node = m.entrySet();
使用m.entrySet();方法,直接把Map集合转为Set集合,并返回泛型Set<Map.Entry<K,V>>类型。
然后直接使用,具体泛型去遍历元素即可

详解第一种:Set< K> =xx. keySet()
返回此映射中包含的键的Set视图,将Map集合中所有的键存入Set集合,然后再通过Set集合的
迭代器取出所有的键,再根据get方法获取每个键的值;

详解第二种:Set<Map.Entry<K,V>> entrySet()
返回此映射中包含的映射关系的Set视图,将Map集合中的映射关系存入到Set集合中,
这个映射关系的数据类型是Map.entry,再通过Map.Entry类的方法再要取出关系里面的键和值
Map.Entry的方法摘要:
boolean equals(Object o) 比较指定对象与此项的相等性。
K getKey() 返回与此项对应的键。
V getValue() 返回与此项对应的值。
int hashCode() 返回此映射项的哈希码值。
V setValue(V value) 用指定的值替换与此项对应的值(特有!!!)。

package Map集合.介绍和遍历;

import java.util.*;
public class Map集合常用方法和遍历 {
    public static void main(String[]args){
        Map<Integer,String> m=new HashMap<>();  //泛型去指定键值对,<Integer,String>
        m.put(0,"ad");
        m.put(1,"ab");
        m.put(2,"ax");
        m.put(1,"ay");//如果key重复,会把前面的key对应的value覆盖掉,直接鸠占鹊巢。
        System.out.println(m.containsKey(1));//true
        System.out.println(m.containsValue("ac"));//false
        System.out.println(m.isEmpty());//false

        m.remove(0);//0,"ad" 会一起消失。一一对应的。

        System.out.println(m.size());//2  数量是键值对的数量

        System.out.println(m.get(0));//null  类似下标获取数组元素,没有也是会有对应的默认值。

        Collection c = m.values();
        for(Object c1:c){
            System.out.println(c1);//ab ax
        }


        //第一种Map集合遍历方法,先用m.keySet();方法遍历取出所有key值对应的Set类型。在遍历key过程中,要取出对应的value即可。
        Set<Integer> set1 = m.keySet();
        
        //循环器foreach形式
        for (Integer key : set1){  ///取出一个key
            System.out.println(key +"="+ m.get(key));//通过每一个key再取出对应的value,然后一起遍历出来即可
        }
        //迭代器Iterator形式,通过Set集合迭代器遍历Set
        Iterator<Integer> it = set1.iterator();
        while (it.hasNext()){
            Integer key = it.next(); //取出一个key
            String value = m.get(key); //这里要多写一步,通过每一个key再取出对应的value
            System.out.println(key+"="+ value);//然后一起遍历出来即可

        }


        //第二种Map集合遍历方法使用entrySet()方法,把整个Map变为Set集合的泛型<Map.Entry<K,V>>即可,效率太高了。
        Set<Map.Entry<Integer,String>> set2 = m.entrySet();

        //foreach形式
        for(Map.Entry<Integer,String> node:set2){
            System.out.println(node);//1=ab 2=ax//这种遍历效率比较高
        }

        //迭代器形式
        Iterator<Map.Entry<Integer,String>> it1 = set2.iterator();
        while (it1.hasNext()){
            Map.Entry<Integer,String> node = it1.next();
            /*Integer key = node.getKey();   //getKey()和getValue();是泛型类的方法,用来提取里面具体的key键和value值的
            String value = node.getValue();*/
            System.out.println(node);
        }

     //第二种的简化版本,把直接使用entrySet()方法,把整个Map变为Set集合,不适用泛型,直接当为Object遍历Set集合也行。
      /*  Set set2 = m.entrySet();
      
        //foreach形式
        for(Object o:set2){
            System.out.println(o);//1=ab 2=ax//这种遍历效率比较高
        }
        //迭代器形式
        Iterator it1 = set2.iterator();
        while (it1.hasNext()){
            Object o = it1.next();
            *//*Integer key = node.getKey();
            String value = node.getValue();*//*
            System.out.println(o);
        }*/
    }
}

Map集合超级无敌重点:底层是哈希表数据结构的集合(特指HashSet类和HashMap类)内部存放的自定义引用类对象都需要重写:hashCode和equals两个方法,后面会讲


8、Map集合接口的HashMap类详解:

一、HashMap类概念:
[1]HashMap哈希表类【数组+链表】:本身的初始化容量是16/指定初始化容量(必须是2的倍数,有利于提高HashMap存取效率和哈希表散列分布均匀
[2]默认是到原数组的75%就自动扩容,扩容倍数是原来容量的2倍
[3]底层的数据结构是哈希表,哈希表就是数组和链表的集合体,数组中每一个元素都是一个链表。这样的结构既继承了数组的优点,又继承了链表的优点。
[4]储存特点:以key-value键值对方式存储元素,key键不可重复,重复就得和对应的value一并被覆盖, value值可重复,key和value一一对应。

二、HashMap面试一定要知道的方法及其原理。(涉及哈希表)
1、增 put(key,value):向集合中添加元素 类似List的add方法
2、取 get(key):获取集合中key键对应的value值

三、HashMap储存数据的流程:(key先调的重写的hashCode取hash值,通过哈希算法得出下标,再调的equals方法比较具体内容是否重复去增删或者查询)
1)首先,先将k和v封装到节点对象中,然后底层调用k的hashCode方法计算出数组下标,下标位置如果没有任何元素,就把节点存储到这个位置。如果说下标有数据即重复了就产生了哈希冲突,拿着k和equals方法,去和其他k作比较,如果返回的都是false就使用拉链法解决哈希冲突即在链表末尾添加节点。如果其中一个返回true,则覆盖旧的value。
2)因此总结:如果HashMap集合分中存放自己定义的类的对象的时候,就要在类中重写equals和hashCode方法:。

重写方法的代码如下展示

package Map集合;
import java.util.*;
public class HashMap{
    public static void main(String[] args) {
        Map<Student,String> m=new HashMap<>();       //泛型 指定Student 和String 类型。

        m.put(new Student("zhangsan"),"1");   //Map集合必须存key-value键值对。
        m.put(new Student("lisi"),"2");
        m.put(new Student("zhangsan"),"3");


        System.out.println(m.get(new Student("zhangsan")));//3  //get方法,通过key得出value。
    }
}


class Student{  //自定义的类对象,要放入HashMap集合中储存必须重写HashCode和equals方法
    String name;
    public Student(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(name, student.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

四、具体讲一下哈希表存储结构(原理面试会考):
1、哈希表也叫散列表,是由一维数组Node[] 内部存储 多个单向链表Node节点的结合体【数组横放+链表纵放,横纵联合一起分布形成哈希表】

PS:纵向单向链表在JDK8后有新特性,如果一个纵向单向链表一旦存超过8个链表节点元素,单向链表数据结构就会自动变为二叉树or红黑树数据结构:
当红黑树上的节点小于6,会重新把红黑树变成单向链表的数据结构

2、接下里源代码理解一下几个属性:

解释源码:
public class HashMap{
transient Node<K,V>[] table;  // 由Node<K,V>[] 数组+链表节点组成,注意:<K,V>是泛型,是引用类,
                                                                        而key-value键值对是元素引用名。
transient Set<Map.Entry<K,V>> entrySet;   //之前学过的,entrySet方法,Map转为Set,返回的是一个Set集合,
//具体类型是泛型<Map.Entry<K,V>>
 static class Node<K,V> implements Map.Entry<K,V> {  //静态内部类HashMap.Node<K,V>和  Map.Entry<K,V>
        final int hash; //hash值即哈希值,是调用哈希表中的K类型去执行hashCode方法,
        //即由哈key键转为为hash希码的一个执行结果。(hash值再通过哈希函数/算法可以转换成数组的下标)
        final K key;  //存储到Map集合中的那个key关键字
          V value;    //存储到Map集合中的那个Value值
        Node<K,V> next;  //存储下一个链表节点的内存地址

总结哈希表中有四个属性:
①Object key :键(key对应value,key不可重复,value可以重复)
②Object value :值(v= map.get(key); )
③int hash: hash哈希码(由key经过重写好的hashCode方法转为hash值)
④ Node<K,V> next:下一个节点的内存地址,默认null
PS:(一个Node<K,V>[]数组下标,代表一个 Node<K,V> 节点,多个Node<K,V>节点形成一个单向链表。)

【文字描述:Hash哈希表的元素实际存在Node<K,V>[]数组套的链表的节点node中,实际就是横向数组套存多个单向链表,数组盒子存的的链表是就会往数组盒子纵向去扩展的,作为拉链法去解决哈希冲突】

3、哈希表内存结构图和方法实现流程:
在这里插入图片描述
4、下面详细介绍哈希表的存储元素的流程图。

注意Hash数据结构的存储特点是:无序不可重复:
①横向使用数组的巧妙之处,
(有映射关系,key可以转为数组下标存地址)
即key值可以通过hashCode方法变为hash值,再去通过哈希函数变为数组下标。 因此每一个数组对应的下标的hash值是一样的,即同一个下标上的链表中的所有节点上存的hash值都是一样的。
②纵向使用链表的巧妙之处。
(链表可以拉链法解决哈希冲突)
哈希冲突:两个key不同,但是却一起通过hashCode方法算出一样的hash值,即可以定位到一样的下标。 因此为了解决同一下标的数组盒子不能存多个元素key,就用拉链法创建链表去纵向链表继续存对象属性。
③因此如果添加元素时,同一数组下标下的所有链表的key元素的equals方法肯定是false,因为是无序不可重复,如果是true就是重复了,就要去覆盖
④反过来查找元素时,如果equals方法返回true,说明两个对象一定在同一下标中的同一纵向的链表上,(说明查询找到了)

①添加元素put方法

map.put(K,V);实现原理 :[先横向数组去找对应下标,找到下标后再去纵向找对应的链表中的key,无重复则添加,重复则key-value一起覆盖]
第一步,肯定是先添加put,先将元素键值对key-value存到Node链表节点对象当中,
第二步,底层会调用引用key的hashCode方法,把key键转为hash值。 然后通过哈希函数(function),去得到具体数组下标,
如果数组下标位置上没有任何元素(元素即链表Node节点),
即一开始该下标的数组盒子上为空盒子null就不存在任何链表,则可以直接把Node链表节点放到该空位,充当该下标盒子的第一个链表元素。
如果数组对应下标已存在链表,则会拿着当前的引用参数key和已存在链表上的key进行equals方法比较,
①如果equals返回false,则说明当前key和已存在链表上的key没有重复,则可以开辟一块新节点空间去添加该元素,注意要Jdk8后使用尾插法,在链表末尾纵向去开辟新链表去添加该元素。
②如果equals返回true,则说明当前key和已存在的的某一个链表的节点中的key重复了,那么就会去覆盖原有的key,同时会去覆盖原有key所对应的value,key-value对应一并覆盖掉。

②查找value方法

v = map.get(key);实现原理: [先横向数组去找对应下标,找到下标后再去纵向找对应的链表的key,无则返回null,有则返回value]
第一步,要查找说明肯定已经放进去put了
第二步,底层还是会调用引用key的hashCode方法,把key键转为hash值。
然后通过哈希函数(function),去得到具体数组下标,
如果数组下标位置上如果没有任何元素,说明不存在元素,查找失败,则get方法会返回null。
如果数组对应下标已存在链表,则会拿着当前的引用参数key和已存在链表上的key进行equals方法比较,
①如果equals返回false,则说明当前key和已存在链表上的key没有重复,即不存在该key键元素,查找失败,则get方法返回null
②如果equals返回true,则说明当前key和已存在的的某一个链表的节点的key重复了,重复即说明找到了该key元素对应的value值,查找成功,则get方法返回该value值

③以上增删和查询方法原理告诉我们,为什么哈希表增删和查询的效率都高?
一、增删都是在链表上增删的。【增删效率会快,地址不连续,删除某个节点,把(要删的节点内部所存的地址属性,即下一个节点地址), 给上一个节点所存的内存地址属性(代替要删的节点的内存地址)即可直接指向下一个节点。】

二、查询也不用全部扫描查找,而是部分扫描查找,【先横后纵,即通过下标去直接定位某一下标的链表,再纵向去一个链表一个链表扫描。】

五、深入什么叫哈希冲突?
(两个不同的key键的hashcode()的hash值却相同,即哈希值冲突了,会得到一样的下标,这时就会用拉链法去纵向存两个key来解决哈希冲突)

【1】详解哈希不冲突和冲突:
①两个相同key的hash值一定相同,则通过哈希函数得到的下标地址也一定相同,一定是存放到同一纵向链表中,(只是这两个相同的key,后放的会去覆盖前放的,因为不可重复。)

②两个不同的key的hash值正常来说是不同的(避免了哈希冲突),则得到的数组下标地址也不一样,因此存放在不同的下标盒子中。(这是正常的不同key的存放方式,hash值正常也不同,存的下标也不同,两个key互相没关系)

③ 但两个不同key的hash值也可能会相同(倒霉遇到了哈希冲突),则也是得到的下标地址也一定相同的,一定也是存放到同一链表中。(并且这两个不同的key不会被覆盖,会另外开辟两个新节点分别存放)

【2】总结总规律:
①两个相同的key, 对应的hashCode(hash值)一定相同。
(但是Map集合不允许重复相同的key,会被覆盖掉先存的)
②反之,两个相同hash值,对应的key却不一定相同(这就叫哈希冲突)
[前提hashCode和equals方法一定要重写好,不然每次new的比较的是key地址都会不同,hash值永远不会一样。]

4、具体实例举例。
比如 :① 一般情况数组下标是通过hash(key)%len获得,也就是元素的key的hash值对数组长度取模/求余得到。
②但在得提前知hashCode一样前提下可以直接key%len获得
比如上述哈希表中,
(哈希表上这几个数字都是在同一数组下标12中的,因此这里等于是提前了说明12、28、108、140四个不同的key的hashCode都是一样的(即哈希冲突了),这样得到的数组下标才会会一样,才会放一起的)
因此在提前得知hashCode一样的前提下就能去具体实操key属性求下标:12%16=12,28%16=12,108%16=12,140%16=12。所以key12、28、108以及140都存储在数组下标为12的位置(前提是hashCode是一样的,才能符合这种哈希算法求余)。

六、深入理解哈希表的优势和劣势?
1、哈希表使用key来存取和增删就等于是查字典时的“首字母”检索法去查字,key先变为下标
用横向数组下标,直接定位到拼音首字母key的第一页链表,再去一页链表一页链表纵向找,这样部分扫描即可。
而不用从字典的第一页一页相当于从头节点链表去一页一页查。
2、哈希表的优势:【先横向后纵向分别完成】
查询,不需要全部扫描从头节点开始一个一个找,只需要部分扫描,直接定位key转为数组下标去链表上查询。
增删,都是在链表上直接增删,不涉及偏移位移
3、不过哈希表没有纯数组查询快,因为哈希表还要通过数组内链表的key转为哈希码再去定位下标,同一下标还要再去一个链表一个链表查存在链表的具体内容元素。
而数组直接定位下标查到具体内容【直接横向时完成】
4、不过哈希表也没有纯链表增删快,因为哈希表增删元素要先进去数组里面的链表属性去删节点,然后指向别的下一个节点地址。
链表是进入链表直接增删指向地址即可。【直接纵向时完成】

七、哈希表的超级重点!!!!(面试题)
1、底层是哈希表数据结构的集合(特指HashSet类和HashMap类),在存放自定义对象类时都需要重写:hashCode和equals两个方法
如果内部存放的是sun公司已经写好的对象类,如Integer/String则不需要重写hashCode和equals两个方法,
2、 为什么要重写HashCode方法?(以保证相同内容的key对象返回相同的hash值,从而可以折叠,确保不添加到重复的元素进入到哈希表中)
3、为什么要重写equals方法?(保证比较的是key具体内容,而不是对象内存地址)

八、哈希函数的设计,要散列分布均匀。
(作用:减少哈希冲突,提高存储效率)

1、哈希表散列分布不均匀?(即哈希表使用不当,变为纯数组或者纯链表)
如果key通过hashCode方法一直返回一个固定hash值,则会固定为一个下标和固定一直纵向去变成纯单向链表存数据。
如果key通过hashCode方法一直返回不固定的hash值,则会下标不会重复,不重复的话则不会纵向伸张链表,就会一直去横向变成纯一维数组存数据。(因此需要重写hashCode)

2、哈希表散列分布均匀:?(你提到hash函数,你知道HashMap的哈希函数怎么设计的吗?)
100个元素 ,横向数组中下标有10个链表数组盒子,每个数组盒子下面再纵向10个链表节点盒子,10*10叫散布均匀。

3、如何散步均匀:?()
①Hash类的构造方法中,放capacity为2的倍数的初始化容量/默认容量16。(初始化容量要设定好)
②需要生成idea的重写hashCode方法。(哈希函数要重写得当,推荐IDE一键生成)

package Map集合;

import java.util.*;
public class 哈希表数据结构 {
    public static void main(String[] args) {
        //举例两个底层为哈希表的集合,HashMap类集合和HashSet类集合
        //HashMap类集合
        //存的是Integer和String,因此sun已经重写好了hashCode和equals两个方法,不用自己手动重写
        Map<Integer,String>  map = new HashMap<>(100);//默认16,自定义必须得是2的倍数,有利于哈希表散列分布均匀
        map.put(111,"zhangsan");
        map.put(222,"lisi");
        map.put(333,"wangwu");
        map.put(444,"king");
        map.put(444,"queen");
        System.out.println(map.size());//自动覆盖 4个

        Set set = map.entrySet();
        for (Object obj : set){
            System.out.println(obj);
        }

         //HashSet类集合
        //存的是Student自定义类,因此必须自己去手动重写好hashCode和equals两个方法,
        // 不然无法达到Set和Map集合中的无序不可重复的特点,即添加元素时无法覆盖一样具体内容的对象类
        Student01 s1 = new Student01("zhangsan");
        Student01 s2 = new Student01("zhangsan");
        Set<Student01> set1  = new HashSet();
        set1.add(s1);
        set1.add(s2);
            System.out.println(set1.size()); //


    }
}

class Student01{
    String name;

    public Student01(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student01 student01 = (Student01) o;
        return Objects.equals(name, student01.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }


}

9、Map集合接口的TreeMap类详解:

一、TreeMap (TreeSet类)基本概念:
底层是二叉树的数据结构,实际是new新建了一个TreeMap,因此TreeSet实际存在TreeMap的key部分。

二、TreeMap存储特点:放无序不可重复元素,但是元素会自动排序,因此二叉树集合类也叫可排序集合(注意有序无序不等于排序)。

三、 注意:(如果是存放的是自定义的对象类,那么就要去重写自定义比较方法,才能去自动排序)。

TreeSet/TreeMap的第一种排序方式是实现Comparable接口覆写compareTo方法比较,(自然排序)
TreeSet/TreeMap的第二种排序方式是实现Comparator接口覆写compare方法比较,(比较器comparator排序)

package Map集合;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class aTreeMap{
    public static void main(String[] args) {

    /*Map<Student1,Integer> m=new TreeMap<>(new Comparator<Student1>() {  //引出匿名内部类,在实际参数里面定义,new+ 接口名+()+{}的方法,去定义接口的方法。
            @Override                                                     //但是这种方法不如直接新建一个类,匿名内部类可读性差。
            public int compare(Student1 o1, Student1 o2) {
                return o1.name.compareTo(o2.name);
            }
        });*/

        //map一定是放key-value 两个元素的。
        //泛型指定:<Student1,Integer>

//        Comparator c = new CompareToStudent();
        Map<Student1,Integer> m = new TreeMap<>(new CompareToStudent());//放new CompareToStudent()   需要无参构造放一个比较类对象实现自带的比较器方法,相当于Comparator c = new CompareToStudent()
        m.put(new Student1("zhangsan",11),1);  //Comparator肯定在TreeMap类被属性实例化了,如下图,在TreeMap某个方法里面再去调comparator属性的方法,等于多态去调实现类的compare方法
        m.put(new Student1("lisi",12),2);   //CompareToStudent like a 充当了 Comparator 去比较学生大小排序 .
        m.put(new Student1("wangwu",20),3);//这个多态Comparator实现方法只有在TreeMap构造方法放了实现类对象才会去执行,不放就会执行另一个构造方法。
        /*
          public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
         public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        */

        Set set=m.entrySet();
        for(Object s:set){
            System.out.print(" "+s);//这里可以使用倒序输出:Student1{age=20, name='wangwu'}=3 Student1{age=12, name='lisi'}=2 Student1{age=11, name='zhangsan'}=1
                                     //比较器比较方法优点,Comparator适合随时修改(升序或降序规则)或者多种比较规则的比较方法):因为是自己额外创建另外一个比较类去实现Comparator接口
        }
        System.out.println();
    }
}

/*
类套类comparator就是为了使顾客 map集合类和自定义比较类CompareToStudent通过comparator接口 产生包含联系,实现具体自定义类的比较方法
把comparator接口对象封装成为map集合类对象的一个实例化类属性,产生包含关系,具体实现comparator接口的实现类CompareToStudent的比较方法。
具体的调用它是在被放套娃类的map集合类的底层源代码中,这个sun已经写好了,不用你自己去调用,你只需要启动这个套娃类,即类构造方法放类属性赋值。
*/


class Student1{  //原来的自动类不需要变动
    int age;
    String name;
    public Student1(String name,int age) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student1{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

//额外重新定义一个比较类,在这个类去实现了Comparator接口(这个类实际就是充当了比较器Comparator), Comparator<Student1> c = new CompareToStudent() 再将这个比较类放到TreeSet构造方法参数中
class CompareToStudent implements Comparator<Student1> {

    @Override
    public int compare(Student1 s1, Student1 s2) {   //这里不能用this.因为出了当前对象的自定义类的作用域。因此直接传两个参数去比较

        if (s1.age != s2.age){
              return  s1.age - s2.age;
        }else{
            return s1.name.compareTo(s2.name);
        }
    }
}

10、Map集合接口的HashTable类详解:

一、HashTable类基本概念:
①HashTable集合的key部分元素和value不允许为null。
②底层也是哈希表数据结构,只是是线程安全的,使用很少,效率很低
(实际开发多用HashMap非线程安全,可以直接用Collections工具类转为线程安全)
③初始化容量11,扩容是原容量的的2倍+1,也是75%时候自动开始扩容。
④HashTable有一个子类Properties类:(也是Map集合一种,也是线程安全的)
Properties类称为属性类(也是map集合),只能是以key-value键值对儿存储“String字符串”)

二、源代码:class Properties extends Hashtable<Object,Object> {}

三、Properties类只需要掌握两个方法:
存储方法:setProperty() = put方法
获取方法:getProperty() = get方

import java.util.Properties;

public class HashTable{
    public static void main(String[] args) {
        Properties properties=new Properties();

        properties.setProperty("ab","ac"); //key-value键值对儿存储“String字符串”
        System.out.println(properties.getProperty("ab"));//通过key取出value
    }
}

大总结:
1、Java集合类型的默认容量以及扩容机制

List集合相关的默认容量以及扩容机制 /Set集合底层是Map,看Map就行
ArrayList
ArrayList默认容量是10
ArrayList最大容量Integer.MAX_VALUE - 8
ArrayList扩容机制,按原数组长度的1.5倍扩容。如果扩容后的大小小于实际需要的大小,将数组扩大到实际需要的大小
LinkedList
LinkedList是用双链表实现的。对容量没有要求,也不需要扩容
Vector
Vector是线程安全版的ArrayList内部实现都是用数组实现的。Vector通过在方法前用synchronized修饰实现了线程同步功能
Vector默认容量是10
Vector最大容量Integer.MAX_VALUE - 8
Vector扩容机制,如果用户没有指定扩容步长,按原数组长度的2倍扩容,否则按用户指定的扩容步长扩容。如果扩容后的大小小于实际需要的大小,将数组扩大到实际需要的大小
Stack
Stack继承自Vector。添加了同步的push(E e),pop(),peek()方法,默认容量和扩容机制同Vector
Stack默认容量是10
Stack最大容量Integer.MAX_VALUE - 8
Stack扩容机制,如果用户没有指定扩容步长,按原数组长度的2倍扩容,否则按用户指定的扩容步长扩容。如果扩容后的大小小于实际需要的大小,将数组扩大到实际需要的大小

Map相关的默认容量以及扩容机制
HashMap
HashMap是基于数组和链表/红黑树实现的。HashMap的容量必须是2的幂次方(原因是(n-1)&hash是取模操作,n必须是2的幂次方)
HashMap默认容量是16
HashMap最大容量2的30次方
HashMap扩容机制,扩容到原数组的两倍
Hashtable
Hashtable默认容量是11(Hashtable默认大小是11是因为除(近似)质数求余的分散效果好:)
Hashtable最大容量Integer.MAX_VALUE - 8
Hashtable扩容机制,扩容到原数组的两倍+1
LinkedHashMap
继承自HashMap扩容机制同HashMap
TreeMap
TreeMap由红黑树实现,容量方面没有限制



2、java各常用集合类是否接受null值(取决sun源代码是否写好给你放,记一记就行了,面试可能问)

① ArrayList/LinkedList:允许多个null值,可以放入不同种类型,也可以放入相同的值
②TreeSet,不允许有null值 (要排序)
TreeMap,是key不允许null,但是value可以为null。
③HashSet/HashMap,key或者value都可以null,HashMap 允许null-null键值对。
④Hashtable , key和value不允许为null

特例说明:
TreeMap、TreeSet两个类在加入第二个元素时,
会调用Comparator比较器比较先后加入的元素是否重复(TreeMap比较的是Key值)。
所以当加入第一个元素时,即使第一个元素是null,也不会报错,
因为此时不会调用比较器,再次加入元素则报错。

已测试的其他集合类HashSet / HashMap / ArrayList / LinkedList
均可接受null值。


import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
 
public class TestNull {
      public static void main(String[] args) {
     //ArrayList
    	 List<String> arrayList = new ArrayList<String>();
    	 arrayList.add(null);
    	 arrayList.add("dd");
    	 System.out.println("ArrayList以上代码运行成功");
    	 
    	 
    //LinkedList
    	 List<String> linkedList = new LinkedList<String>();
    	 linkedList.add(null);
    	 linkedList.add("ddd");
    	 System.out.println("LinkedList以上代码运行成功");
	}
} 
     //TreeSet
    	 Set<String> treeSet = new TreeSet<String>();
    	 //以下两行代码执行时,会报错。理由同TreeMap
    	 //treeSet.add(null);
    	 treeSet.add("sss");
    	 System.out.println("TreeSet以上代码运行成功");	 

    //TreeMap  允许value值为null,不允许key值为null
    	 TreeMap<String,String> treeMap = new TreeMap<String,String>(); 
//Map放入第一个元素时不会调用比较器,所以不会调用比较器,不会出现NullPointerException
//以下一行代码执行时不会报错,但当treeMapp中放入元素大于1时,就会调用比较器,出现NullPointerException
// treeMap.put(null, null);
//treeMap.put(null,"ddd");
    	 treeMap.put("ddd", null);
    	 treeMap.put("sss", null);
    	 System.out.println("TreeMap以上代码运行成功");
    	 

    	 
// Hashtable  key部分元素和value不允许为nul      
    Hashtable hashtable = new Hashtable();
//        hashtable.put(1,null); 都报错
//        hashtable.put(null,1);
//        hashtable.put(null,null);
        System.out.println("以上代码运行成功");   
           	 	 
//HashSet
    	 Set<String> hashSet = new HashSet<String>();
    	 hashSet.add(null);
    	 hashSet.add("ddd");
    	 System.out.println("HashSet以上代码运行成功");
    	 
//HashMap  允许null-null键值对
    	  Map<String,String> hashMap = new HashMap<String,String>();
    	  hashMap.put("11", "ddd");
    	  hashMap.put("1233", null);
    	  hashMap.put(null, "wang");
    	  hashMap.put(null, null);
    	  System.out.println("HashMap以上代码运行成功");  
 
    

在实际开发中、如何选择数据结构!!!!!
1>Array :读快-写慢
2>Linked:改快-读慢
3>Hash :间于两者之间
之前我们认识到:
数组:便于查询和修改 (查和改),不便于删除和插入(有下标很快的找到和修改,插入删除还要移位) ,但是尾插法不需要位移。
链表:便于删除和插入 (增和删),不便于查询和修改(无下标 查找修改必从链头开始,删除和插入直接连接即可)


六、IO流

一、什么是IO?
(Input输入和Output输出)

二、什么是IO流?
[1](InputStream输入流和OutputStream输出流)
[2]从内存里面出来叫输出、往内存里面去叫输入(输出输入都是相对于内存来说)
[3]作用:通过IO流可以完成内存对硬盘文件的读(输入)和写(输出)。

三、IO读取方式流动图:(输出输入都是相对于内存来说)

输入流(InputStream)、读取(Read) 、输入(Input)、
内存(文件) <------ 硬盘(文件) (输入:word文件从某个硬盘的内部地址导出读取)
输出流(OutputStream)、 写(Write) 、输出(Output)、
内存(文件) -----—-> 硬盘(文件) (输出:word文件保存写进入某个硬盘的内部地址)

四、IO流两大类:(字节输出输入流、字符输出输入流)
1、字节流(万能):(最常用以Stream结尾)
a(一个字节一个字节读取)
b 按照字节读取,基本上任何类型文件都可以读取(任何文本、图片,视频、声音)
c 举例:文件a中国b美国 读取顺序;'a’字符(1字节)——中字符的一半字节——中字符另一半字节-----(字节流比较细化)

2、字符流:(比较少用以Reader/Writer结尾)
a(一个字符一个字符读取)
b 按照字符读取,只能读取普通文本文件。(只能纯txt文本,连word带有格式的文本都无法读取)
c 举例:文件a中国b美国 读取顺序:'a’字符(1字节)——中字符(2字节)----------(字符流比较粗糙)

3、(重点记住!!!:一定用结尾区分字节or字符流:以Stream结尾都是字节流、以Reader/Writer结尾都是字符流。)

五、IO流注意点:
【1】java中IO流这块四大父类家族流(实际是一个抽象类):
java中所有的流都在 java.io.*包下
1、java.io.InputStream: 字节输入流 (导入)
2、java.io.OutputStream:字节输出流 (保存)
3、java.io.Reader: 字符输入流
4、java.io.Writer: 字符输出流
【2】File文件流是其他包装流流的根基,下面其他流都是包装流,他们的构造方法都要放文件专属流对象(即流套流产生联系,使得包装流可以以文件输出的形式输出或输入)
【3】所有流都实现了java.io.Closeable接口,都有close方法都是可关闭的。而流毕竟是一个硬盘和内存之间的管道,
【4】不管输出or输入流使用后都必须使用关闭close(),不然浪费大量资源空间。
【5】所有输出流都实现了java.io.Flushable接口,都有flush方法,都是可刷新(清空流管道)的。
因此所有的输出流在输出之后都要记得写刷新flush()和close关闭方法。否则可能丢失数据

六、学习文件File专属流即可,其他专属子流都是照葫芦画瓢。
【1】read(无参/有参)方法

[1]FileInputStream中常用的方法:
1、read():从头字符开始读取对应字节,返回类型为int,如果没有字节了就返回-1。
2、read(byte[]):将字节转读到byte数组中的个数,返回一次能读到byte数组字符所含字节的数量。(读到的数量取决于数组的自定义容量),如果读到没有字节了就返回-1
3、int read(byte[] b, int off, int len)=从该输入流读取最多 len字节的数据为字节数组。
3、available():返回流当前剩余可读到的字节数量。没有字节可读取就返回0(如果从源头输出这个方法,即相当于知道这个文件有总字节数量)
4、skip(long n)跳过几个字节不读

【2】Write(有参)方法

[2]FileOutputStream中常用的方法:
void write(byte/char[] b) = 将 b.length个字节或字符从指定的字节数组写入此文件输出流。
void write(byte/char[] b, int off, int len) = 将 len字节或字符从位于偏移量 off的指定字节数组写出此文件输出流。
void write(int b) = 将指定的字节写入此文件输出流。

【3】需用用到new String();

byte[] b =s.getBytes(); = 把String字符串——byte数组
new String(bytes) = (byte数组全部——String)
new String(bytes,0, 4) = 把byte数组部分——String输出。
从下标0开始,一次所读到4个字节,就返回对应数量的字符(取决于byte容量)
注意:一个字母字符或者空格在java中占1字节,汉字字符占3字节。

1、读取和写出文件模板套路:(文件字节流)
死记以下两种代码模板,可以分批/一次性输出文件的所有内容:
【1】FileInputStream字节流读取文件:字节用byte[]数组:

package IO流.File文件专属流.File专属字节流;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStream中read有参方法读文件 {
    public static void main(String[] args) {
    
FileInputStream fis = null;
        try {
            fis = new FileInputStream("D:\\前端和java资料\\javaIO流txt文件.txt");
   //第一种:自定义数组容量。在while循环内
   //特点:在循环内:分批次循环读取字节串 + 分批次String构造方法byte转字节串输出(就是分批次读到几个字节输出几个字节串)
         byte[] bytes =new byte[4];
            int readCount = 0; //readCount变量就代表就是一次所读取到的字节数量,这样不会写死,一次读到几个字节就转为几个字节
            while((readCount =fis.read(bytes)) != -1){
            // System.out.print(new String(bytes)); 一次性全部转,因为已确保全部读完了。
             System.out.print(new String(bytes,0,readCount)); // abcdefga  这里是分批次读取,把byte数组一次所读到几个字节,就返回对应的字节数量(取决于byte容量,读到几个转几个String)
                                                                 //第一次读到abcd,第二次读到efga,最后没有字节就会false退出循环

    //第二种,fis.available()控制数组最大容量。不用while循环
   // 特点:一次性read读出全部字节串 + 一次性String构造方法byte转字节串输出(就是一次性读全部字节,再全部转为字节串)
        byte[] bytes =new byte[fis.available()];
       int readCount = fis.read(bytes);//一次性把在byte数组的字节内容全部读取,因为数组的最大容量为字节串全部内容
        System.out.print(new String(bytes,0,readCount)); //

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

【2】FileOutputStream字节流写出文件,用byte[]数组:
①FileOutputStream中常用的方法:(输出流,负责写)
1、文件字节输出流,(内存write写进—>硬盘)
1、write(byte[]),将byte数组写入创建好的文件。
2、构造方法(name"文件名",append:是否追加true/false)
3、所有的输出流在Write写完之后都要记得写刷新flush()和close关闭方法。否则可能丢失数据
4、 append true只是为了保存之前已存在文件的内容,防止误删。真正write轮流写入时,是不会覆盖的,一次一次轮流写入硬盘

②流程:
1、 创建输出流管道和文件名 new FileOutputStream(“cs.txt”,true);
2、创建byte数组 或者 字符串再转数组。 byte[] bytes = {97,98,99,100}; byte[] b =s.getBytes();
3、write方法写出byte数组即可(不用再转为字符串,电脑硬盘会自动转)
4、ps:如果new输出流的时候,不选择追加则第一次写入时就会先清空之前内容再重新写入
如果new输出流的时候,选择追加true则第一次写入时不会清空之前内容,会继续写入文档内.
5、
新建的文件不用加true,如果是已经存在内容的文件,就要加true,(不然误删清空之前内容)
true只是为了:
1、保存之前已存在文件的内容,防止误删。
2、同时也可以保存你新建文件之后写的内存,你再次去手动修改已写好内容,那么之前内容也还会在。
3、但不管有没有加true,真正write轮流写入时,是不会覆盖的,内存的内容数据会一次一次轮流写入硬盘。

package IO流.File文件专属流.File专属字节流;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStream中常用的方法 {
    public static void main(String[] args){
        FileOutputStream fos=null;
        try {
            fos= new FileOutputStream("D:\\java idea\\idea代码文件\\IO流\\src\\IO流\\自定义写入的文档",true);
//            byte[] bytes = {97,98,99,100};   直接自己定义一个byte数组也行,但是这个是byte是ASCII码转为对应字符的。
           //定义字符串也行,但要转为byte才行。
            String s="我是中国人";
            byte[] b =s.getBytes();
            fos.write(b);//把byte数组全部写出

            String s1="加了true的话,之前文件已经存在的内容是不会被覆盖的";
            byte[] b1 = s1.getBytes();
            fos.write(b1);  //我是中国人我同时也是java菜鸟弟弟工程师-------->我是中国人我同时也是java菜鸟弟弟工程师  我是中国人加了true的话,之前文件已经存在的内容是不会被覆盖的
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if(fos!=null){
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

2、读取和写出文件模板套路:(文件字符流)
【1】FileReader输入字符流:字符要用char[]数组
①字符流:(以Reader/Writer结尾)
a(一个字符一个字符读取)
b 功能:按照字符方式去读取,只能读取普通文本文件。(只能纯txt文本,连word带有格式的文本都无法读取)
c 举例:文件a中国b美国 读取顺序:'a’字符(1字节)——中字符(2字节)----------(字符流比较粗糙)

②为什么字符流要使用char数组,因为字符流计算机读取出来的是字符形式,因此要转放到char数组里面去转读,方便可以去再把byte数组转为字节串形式输出。

package IO流.File文件专属流.File专属字符流;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReader输入字符流 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = null;
        try{
            fileReader = new FileReader("D:\\前端和java资料\\javaIO字符流txt文件.txt");

         char[] chars = new char[4];
            int readCount = 0;
            while((readCount = fileReader.read(chars))!= -1){  //一次从char数组读出几个字符,就转为对应数量的字符。(这里一次可读四个字符。)
                //一个字符就是一个字符
                System.out.print(new String(chars,0,readCount));
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileReader != null){
                fileReader.close();
            }
        }
    }

}

【2】 FileWriter输出字符流:要用char[]数组
字符输出流write的比字节输出流好处在于可以直接写出:
字符串或者char写出都行。(但是只能是普通的文本。)
优点:可以直接写String字符串输出
缺点:自身字符流,只能是普通txt文本文件。

public class FileWriter输出字符流 {
    public static void main(String[] args) throws IOException {
        FileWriter fileWriter = null;
        try{
            fileWriter = new FileWriter("IO流\\src\\IO流\\文件专属字符流\\自定义字符输出流文件");//idea是以project作为起始文件目录的
            //第一种方式创建 char数组,直接读出
            char[] chars = {'中','国','人'};
            fileWriter.write(chars);

            //第二种,字符流可以直接写入String,字节流写完String还要getBytes转为byte数组才能写出
            String  s = "我是中国人";
            fileWriter.write(s);
            fileWriter.write("\n"); //换行
            fileWriter.write("我是一名java软件工程师");



            fileWriter.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileWriter != null){
                fileWriter.close();
            }
        }
    }

}

六、文件拷贝:
1、(字节流文件拷贝):

/*
1、文件内容复制就是拷贝文件:C硬盘的文件-----》D硬盘的文件  中间在内存中转换(先读C盘的文件,再写入D盘中)
2、然后就是一边读取一边写出。(字节流任何文件类型都行)
3、两个流一起使用时,关闭close,要分开try去close关闭.
4、流程:一边读,一边写。中间不用在内存中输出。直接write写出即可
          byte[] b=new byte[1024*1024];//一次最多拷贝1MB
            int readData=0;
            while ((readData=i.read(b))!=-1){  一边读
//                System.out.println(new String(b,0,readData));中间这里不用在内存中输出,因为是要直接拷贝到别的文件中去,直接写出即可
                o.write(b,0,readData);    一边写
            }
            o.flush();//最后一定要刷新
*/
package IO流.File文件专属流.File专属字节流;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class a字节流文件拷贝 {
    public static void main(String[] args) {
        FileOutputStream o=null;
        FileInputStream i=null;
        try {
            i=new FileInputStream("D:\\java idea\\idea代码文件\\IO流\\src\\IO流\\自定义写入的文档"); //输入流要提前写好文件名,不然访问报错
            o=new FileOutputStream("D:\\java idea\\idea代码文件\\IO流\\src\\IO流\\自定义写入的文档2");//输出流不用提前写文件,没有的话系统会自动生成,新建文件不用加true
            //最核心,怎么写(一边读取,一边写入)
            byte[] b=new byte[1024*1024];//一次最多拷贝1MB

            int readData=0;
            while ((readData= i.read(b))!=-1){
//                System.out.println(new String(b,0,readData));这里不用在内存中输出,因为是要直接拷贝到别的文件中去,直接写出即可

                o.write(b,0,readData); //一次读多少,就写出多少,俗称拷贝
                //自定义写入的文档---->自定义写入的文档2(拷贝之后,两个文件内容一致了)
            }
            o.flush();//最后一定要刷新
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //两个流一起使用时,关闭close要分开关闭.
            if(o!=null){
                try {
                    o.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(i!=null){
                try {
                    i.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

2、(字符流文件拷贝):

package IO流.File文件专属流.File专属字符流;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class 字符流文本拷贝 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = null;
        FileWriter fileWriter = null;

        try{
            fileReader =  new FileReader("IO流\\src\\IO流\\文件专属字符流\\自定义" +
                    "字符输出流文件"); //文件最初位置。
            fileWriter = new FileWriter("IO流\\src\\IO流\\文件专属字符流\\自定义字符输出流文件2");//拷贝之后的位置。

            char[] chars = new char[1024*1024];
            int readCount = 0;
            while ((fileReader.read(chars)) !=-1){
                fileWriter.write(chars,0,readCount);
            }
            fileWriter.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (fileWriter != null){
                fileWriter.close();
            }
            if (fileReader != null){
                fileReader.close();
            }
        }
    }

}

七、拷贝目录(java.io.File):
1、File类和四大IO流家族没有关系,是io流下的单独的一个类。
2、自己解释:计算中的万物都是File,计算机的软件,文件目录,路径,都是File。(File就是文件路径名的抽象的表示方法,只是在计算机中用图示化的方式来呈现给用户)

3、需要掌握File类中的方法(电脑系统文件管理和查看)

【1】创建:
createNewFile()在指定位置创建一个空文件,成功就返回true,如果已存在就不创建,然后返回false。
mkdir() 在指定位置创建一个单级文件夹(单个文件夹)。
mkdirs() 在指定位置创建一个多级文件夹(套娃文件夹)。
renameTo(File dest)如果目标文件与源文件是在同一个路径下,那么renameTo的作用是重命名,
如果目标文件与源文件不是在同一个路径下,那么renameTo的作用就是剪切,而且还不能操作文件夹。

【2】删除:
delete() : 删除文件或者一个空文件夹,不能删除非空文件夹,马上删除文件,返回一个布尔值。
deleteOnExit():jvm退出时删除文件或者文件夹,用于删除临时文件,无返回值。
【3】判断:
exists() 判断文件或文件夹是否存在。
isFile() 判断是否是一个文件,如果不存在,则始终为false。
isDirectory() 判断是否是一个文件夹目录,如果不存在,则始终为false。
isHidden() 判断是否是一个隐藏的文件或是否是隐藏的目录。
isAbsolute() 判断此抽象路径名是否为绝对路径名。(绝对路径,从当前磁盘下找。相对路径,从一个磁盘跳到另一个磁盘下找)
【4】 获取:
getName() 获取文件或文件夹的名称,不包含上级路径。
getPath() 获取相对路径 IO流\src\IO流\Data数据专属流\数据专属流自定义文档
getAbsolutePath()获取文件的绝对路径,与文件是否存在没关系 D:\java idea\idea代码文件\IO流\src\IO流\Data数据专属流\数据专属流自定义文档
length() 获取文件的大小(字节数),如果文件不存在则返回0L,如果是文件夹也返回0L。(去看文件右键自己看大小对比)
【1】getParent() 返回的是String,即此当前文件抽象路径名的父目录的路径名(即上一级的父目录);如果此路径名没有指定父目录,则返回null。
【2】getParentFile(); 返回的是一个File类,也是即获取当前文件的父目录路径。
IO流\src\IO流\Data数据专属流\数据专属流自定义文档 —》返回 IO流\src\IO流\Data数据专属流
getParent()和getParentFile()区别在于,都是获取父路径,一个返回String,一个返回File类
lastModified() 获取文件最后一次被修改的时间。(就是当前系统文件最后一次访问和修改时间)

【5】关于文件夹目录(文件不能用)相关:
1) listRoots() 列出所有的根目录(就是Window中就是所有系统的盘符),(返回File[]数组) + foreach遍历
2)list() 返回目录下的所有文件或者目录名,包含隐藏文件。对于文件这样操作会返回null。(返回String[]数组即所有子文件路径名) + foreach遍历
3)listFiles() 返回目录下的所有文件或者目录对象(File类实例),包含隐藏文件。对于文件这样操作会返回null。(返回File[]数组子文件对象也是路径名) + foreach遍历
下面这两个了解即可,不常用
list(FilenameFilter filter)返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。
listFiles(FilenameFilter filter)返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。

4、方法代码展示:

public class File类简介和常用方法 {
    public static void main(String[] args) throws IOException {
        //创建方法
/*        @SuppressWarnings("unused")
        File file = new File("F:\\File.txt");
        //System.out.println("创建成功了吗?"+file.createNewFile());
        //System.out.println("单级文件夹创建成功了吗?"+file.mkdir());
        //System.out.println("多级文件夹创建成功了吗?"+file.mkdirs());
        //File dest = new File("F:\\电影\\c.txt");
        //System.out.println("重命名成功了吗?"+file.renameTo(dest)); //把文件名修改了,返回boolean是否修改
*/

/*      //删除方法
        File file = new File("F:\\电影");
        System.out.println("删除成功了吗?"+file.delete());//删除文件或者一个空文件夹,不能删除非空文件夹,马上删除文件,返回一个布尔值。

        file.deleteOnExit();//jvm退出时删除文件或者文件夹,用于删除临时文件,无返回值。
*/

        //判断方法
/*        File file = new File("F:\\a.txt");
        System.out.println("文件或者文件夹存在吗?"+file.exists());
        System.out.println("是一个文件吗?"+file.isFile());
        System.out.println("是一个文件夹吗?"+file.isDirectory());
        System.out.println("是隐藏文件吗?"+file.isHidden());
        System.out.println("此路径是绝对路径名?"+file.isAbsolute());
*/

        //获取方法

         File file1 = new File("IO流\\src\\IO流\\Data数据专属流\\数据专属流自定义文档");
        System.out.println("文件或者文件夹得名称是:"+file1.getName());

        System.out.println("相对路径是:"+file1.getPath());
        System.out.println("绝对路径是:"+file1.getAbsolutePath());
        System.out.println("文件大小是(以字节为单位):"+file1.length());

        String s2 = file1.getParent();
        System.out.println("父路径是"+s2); //都是获取父路径,一个返回String,一个返回File类对象
        File f1 = file1.getParentFile();
        System.out.println("父路径是"+f1);//都是获取父路径,一个返回String,一个返回File类对象

        //使用日期类与日期格式化类进行获取规定的时间
        long  lastmodified = file1.lastModified();
        Date date = new Date(lastmodified); //要是Date有构造方法参数(毫秒),则对象输出sun的给的默认时间的形式(Thu Jan 01 00:00:00 CST 1970)
        System.out.println(date);//Sat Oct 24 14:09:42 CST 2020  这里为这种形式不好看
        SimpleDateFormat simpledataformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        System.out.println("最后一次修改的时间是:"+simpledataformat.format(date));  //修改为正常形式。


        //文件或者文件夹的方法
        //
        File[] file = File.listRoots();
        System.out.println("所有的盘符是:");
        for(File item : file){
            System.out.println("\t"+item);
        }

        //filename.list(); = 返回String数组即所有子文件路径名
        File filename =new File("D:\\前端和java资料\\资料");
        String[] name = filename.list();
        System.out.println("指定文件夹下的文件或者文件夹有:");
        for(String item : name){
            System.out.println("\t"+item);
        }

        // filename.listFiles() = (返回File数组子文件对象也是路径名)
        File[] f = filename.listFiles();
        System.out.println("获得该路径下的文件或文件夹是:");
        for(File item : f){
            System.out.println("\t"+item.getName());
        }

    }
}

5、如何具体拷贝目录:

一、首先先回顾拷贝文件????????????、
1、拷贝文件:C硬盘的文件-----》D硬盘的文件  中间在内存中转换(先读C盘的文件,再写入D盘中)
2、然后就是一边读取一边写出。(字节流任何文件类型都行)
3、两个流一起使用时,关闭close,要分开try去close关闭.
4、流程:一边读,一边写。中间不用在内存中输出。直接write写出即可
          byte[] b=new byte[1024*1024];//一次最多拷贝1MB
            int readData=0;
            while ((readData=i.read(b))!=-1){  一边读
                System.out.println(new String(b,0,readData));中间这里不用在内存中输出,因为是要直接拷贝到别的文件中去,直接写出即可
                o.write(b,0,readData);    一边写
            }
            o.flush();//最后一定要刷新

二、拷贝目录(即拷贝文件夹)?????????????????
拷贝目录:“拷贝起点文件夹”

*/package File类简介;

import java.io.*;

public class File拷贝目录案例 {
    public static void main(String[] args) {
        //拷贝源头
        File srcFile = new File("D:\\拷贝目录\\a\\b\\拷贝起点");
        //拷贝目标(注意连路径都要一起拷贝的)
        File destFile = new File("C:\\拷贝目标1\\新建文件夹");
        //调用方法拷贝
        copyDir(srcFile,destFile);

    }


    /**
     * 拷贝目录
     * @param srcFile
     * @param destFile
     */
    private static void copyDir(File srcFile, File destFile){
        if (srcFile.isFile()){  //如果srcFile一开始就是一个文件,则递归结束
            //如果是一个文件,递归结束。
            //同时还需要拷贝该文件
            //一边读一边写
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                //读取文件
                in = new FileInputStream(srcFile);//构造方法还可以放一个文件
                //写出文件
                String path =  destFile.getAbsolutePath() +"\\"+ srcFile.getAbsolutePath().substring(3);
                out = new FileOutputStream(path);//待定
                //一边读一边写
                byte[] bytes = new byte[1024*1024];
                int readCount = 0;
                while ((readCount = in.read(bytes))!=-1){
                    out.write(bytes,0,readCount);
                }

                out.flush();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if (in!=null){
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (out!=null ){
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return;
        }
        //程序执行到这里说明,src是一个文件夹目录
        File[] file1 =  srcFile.listFiles();  //取出src文件夹录下面所有的孩子(文件or文件夹),返回一个File数组
        for (File file :file1){ //再用foreach循环遍历孩子数组,取出所有孩子中的一个file,这个file可能是文件也可能是目录
//            System.out.println(file.getAbsolutePath());//因此不清楚是文件还是目录,就把他的路径全部看一遍。
            /*
            D:\拷贝目录\a\b\拷贝起点\copy1
            D:\拷贝目录\a\b\拷贝起点\copy2
            */

            if (file.isDirectory()){  //如果file是一个目录,就去C盘新建对应的目录。
               // System.out.println(file.getAbsolutePath());
               //D:\拷贝目录\a\b\拷贝起点
               //C:\拷贝目标1\新建文件夹\拷贝目录\a\b\拷贝起点
                // 注意:这里连路径都要复制拷贝
                String srcDir = file.getAbsolutePath();
                String destDir = destFile.getAbsolutePath() +"\\"+ srcDir.substring(3);
                /*
                 System.out.println(destDir);
                C:\拷贝目标1\新建文件夹\拷贝目录\a\b\拷贝起点
                C:\拷贝目标1\新建文件夹\拷贝目录\a\b\拷贝起点
                */
                File newFile = new File(destDir); //目标文件路径
                if(!newFile.exists()){
                    newFile.mkdirs();//如果文件不存在,就创建对应的目录
                }

            }

            copyDir(file,destFile); //使用copy方法递归,递归到上面的if,直到确定file是一个纯文件我们再去拷贝,目录就递归。(这里的file继承了 srcFile和file1)

        }


    }
}

六、IO流和Properties联合使用:
一、什么是Properties????
③HashTable有一个子类Properties类:(也是Map集合一种,也是线程安全的),
Properties类称为属性类(也是Map集合),只能是以key-value键值对儿存储“String字符串”)
④只需要掌握两个方法:
存储方法:setProperty() = put方法
获取方法:getProperty() = get方法 返回String
(这个后期可以和反射联合,读取配置文件中的反射class对象)

二、什么是IO流?
文件的读和写。。。。。(不要复杂化)

三、IO流+ Properties类联合使用???
实现:要把自定义文档的数据,放到 Properties数据中。
步骤:
1、使用属性类load方法把文件数据加载到到 Properties类中(map集合)
2、再使用getProperty()方法把自定义文件中key对应value读取出来
3、前提是文件存的数据一定是对应关系的,比如账号登陆。
key1= value
key2=value
(要使用=号,且最好不要有空格)
username = tuyuexin (username改为xxxx)
password=123123123 (password改为xxx)

四、总结:
1、(这种用于修改数据的自定义文件专门叫配置文件,要以properties结尾,)
2、好处:以后要修改的数据,就把这些数据单独放到一个自定义的文件中,将文件中修改好的数据,通过“IO流+ Properties类联合使用”,去读取到java内存中。
3、作用:建议以后修改类中的属性数据,只要修改文件的数据即可,不要修改原来的写好的java代码数据,要重新编译和重启服务器,很麻烦。

package IO进阶;
import java.io.FileInputStream;
import java.util.Properties;

public class IO流和Properties联合使用 {
    public static void main(String[] args) throws Exception{
        //先读取硬盘的自定义文档
        FileInputStream in = new FileInputStream("IO流\\src\\IO进阶\\自定义文档");

        /*byte[] bytes = new byte[in.available()];
        int readCount = in.read(bytes);
        System.out.println(new String(bytes,0,readCount));*/

        //使用Properties类的load方法,可以将文件的数据直接加载到Map集合即Properties中
        Properties pro = new Properties();  //等号左边为key,右边为value
        pro.load(in);

        //最后使用Properties类的getProperty()  = get方法去获取key对应的value值
        System.out.println(pro.getProperty("username"));
        System.out.println(pro.getProperty("password"));
    }
}

七、简单学习一下BufferedReader专属流(包装流):
1、包装流VS节点流:
①如果一个流的构造方法需要另一个流对象。则外部这个流被称为“包装流”,而构造方法放的流被称为“节点流”
②包装流BufferedReader作用:去包装一个“节点流”,
③节点流FileReader作用:作为包装流的构造参数,两者相辅相成
例如下面的程序,包装流是BufferedReader,节点流是FileReader)。

2、Buffered专属流简介:
①缓冲流Buffered专属流:(这个流优点就是可以不用自定义byte/char数组,自带数组缓冲区,直接读or写(仅限普通文本),节约内存

②Buffered专属流的构造方法传的是Reader老父类对象:
即Buffered流的类构造方法值只能放的是一个Reader流的类对象(套娃——类套类)
但是注意Reader是抽象类,无法实例化对象,但是他有子类儿子FileReader/InputStreamReader是普通类可以放。
继承图:子FileReader——子InputStreamReader——Reader父类

③因此总结:
[1]Buffered流只能是从别的字符/字节转字符的节点流转化过来,自身并不能读取文件!!!
[2]Buffered流的类构造方法放的是字符流:即Reader抽象父类流的子类流对象(子类继承并且充当父类去放到构造方法中)。
(具体是FileReader或者InputStreamReader子类对象,要要先new子类对象,再放进去Buffered构造方法中)

3、如何使用Buffered专属流读取文件:
用 bufferedReader.readLine();特有输出方法 ,从文本源头开始一行一行读取文本行。+ sout(返回一个String,如果读到没有文本行就返回一个String = null)

【1】文件字符流转为Buffered专属流:(直接转然后读取即可)

package IO流.Buffered缓冲区专属流;
import java.io.BufferedReader;
import java.io.FileReader;
public class BufferedReader{
    public static void main(String[] args)throws Exception {
        FileReader fileReader = new FileReader("IO流\\src\\IO流\\Buffered缓冲区专属流\\Buffered流自定义文档");  //先new Reader父类的子类对象FileReader.
        BufferedReader bufferedReader = new BufferedReader(fileReader); //然后 子类对象继承Reader父类对象去放入到Buffer缓冲流的构造函数中。

        //Buffered自带缓冲流不用创建自定义数组,直接读or写(仅限普通文本).
//        String s = bufferedReader.readLine();   输出一次,就是输出文本的一行
//        System.out.println(s);

        String s = null;  //while循环变量篮子放外面,每次循环完都更新,可以看返回值是否为nul或者0(即这里可以看出是否已经读完了文本)
        while ((s=bufferedReader.readLine()) !=null){     //如果他返回的不是一个null。
            System.out.println(s);//一次读一行,直到他读不到返回一个null时。
        }


        //任何流都要记得关
       bufferedReader.close();
    }
}

【2】文件字节流转为Buffered专属流:
(字节先转为字符流,再转为Buffered专属流,然后读取即可)
1、转换专属流:(把字节流转为字符流)
InputStreamReader 、OutputStreamWriter

2、如何使用转换流:
InputStreamReader reader = new InputStreamReader(fis);
//使用转换流去把字节流转为字符流,任何专属字节流都可以用“转换专属流”转为字符流,但是是输出还是输入得分清

3、最后读取Buffered专属流即可:

package IO流.Buffered缓冲区专属流;
import java.io.*;
public class BufferedReader流和转换流使用 {
    public static void main(String[] args) throws IOException {
       /* FileInputStream fis = new FileInputStream("Buffered流自定义文档");
        BufferedReader bufferedReader = new BufferedReader(fis); //直接放fis会报错,要使用转换流去把字节流转为字符流*/

        FileInputStream fis = new FileInputStream("IO流\\src\\IO流\\Buffered缓冲区专属流\\Buffered流自定义文档");
        InputStreamReader reader = new InputStreamReader(fis);//使用转换流去把字节流转为字符流

        BufferedReader bufferedReader = new BufferedReader(reader); //构造方法只能是字符流

        String s = null;
        while((s=bufferedReader.readLine())!= null ){
            System.out.println(s);
        }


        bufferedReader.close();

    }
}

八、简单学习数据Data专属流:(DataInputStream、DataOutputStream )
(这个流必须要将数据和数据本身的类型一起传入或者读取出来,所以用的很少)
(数据都字节流,因为字节流可以读任何类型文件)

2、这个流所写出的文件,为加密文件不是普通文件,(不能使用普通本文记事本打开,因此很少用)

3、要固定Write所读数据的类型,要将数据和数据本身的类型一起传入或者读取出来,所以用的很少
4、数据专属流,要提前知道写入时的顺序,因为读取顺序必须和写入的顺序一致

5、缺点:
①数据专属流没有自定义数组缓冲区,读取也不能使用循环读,只能用特点readByte()/readChar()方法一个数据类一个数据类型读,顺序还要和写入的顺序一致
②数据专属流写出时不能直接写出,要用特定类型的WriteByte(b)/writeChar©方法写出。

dataOutputStream.writeByte(b);
dataOutputStream.writeChar©;
dataOutputStream.writeShort(s);
dataOutputStream.writeInt(i);
dataOutputStream.writeDouble(d);
dataOutputStream.writeFloat(f);
dataOutputStream.writeBoolean(b1);
dataOutputStream.write(s1.getBytes());

读取
package IO流.Data数据专属流;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class DataInputStream{
    public static void main(String[] args) throws Exception {
        DataInputStream dataInputStream = new DataInputStream(new FileInputStream("IO流\\src\\IO流\\Data数据专属流\\数据专属流自定义文档"));

        //不能使用循环读出来,因为读出来的类型和写出来的类型要和写入时一样
        //只能一个一个数据类型读取,并且顺序还要一致
        dataInputStream.readByte();
        dataInputStream.readChar();
        dataInputStream.readShort();
        dataInputStream.readDouble();
        dataInputStream.readFloat();
        dataInputStream.readBoolean();
        dataInputStream.readByte();

        dataInputStream.close();
    }
}

写出
package IO流.Data数据专属流;
import java.io.*;
public class DataOutputStream{
    public static void main(String[] args) throws Exception {
        DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("IO流\\src\\IO流\\Data数据专属流\\数据专属流自定义文档"));

        //写数据
        byte b = 1;
        char c = 'a';
        short s = 123;
        int i = 1314;
        double d = 3.14;
        float f = 3.14f;
        boolean b1 = false;
        String s1 = "啪啪啪";

        //必须要固定输出数据的数据类型,比较麻烦
        dataOutputStream.writeByte(b);
        dataOutputStream.writeChar(c);
        dataOutputStream.writeShort(s);
        dataOutputStream.writeInt(i);
        dataOutputStream.writeDouble(d);
        dataOutputStream.writeFloat(f);
        dataOutputStream.writeBoolean(b1);
        dataOutputStream.write(s1.getBytes());
        dataOutputStream.flush();
        dataOutputStream.close();
    }
}

九、标准输出print专属流:(可以指定终端信息打印位置,默认输出到控制台)
PrintReader、 PrintWriter (打印都字符流,因为打印必须以String形式输出的)

1、输出流的特点???
【1】标准输出print专属流默认输出到控制台,因此不用写文件路径直接:
PrintStream printStream = System.out;

【2】用标准流的println打印方法输出:
printStream.println(“我喜欢你”);

【3】最后不用使用关闭方法

3、可以改变或者指定,终端信息打印位置(默认是打印到控制台 System.out)
//此时不再指向控制台,指向的是new新对象FileOutputStream的文本文件
第一步、先new指向的写出数据的文件,包装流。
PrintStream printStream1 = new PrintStream(new FileOutputStream(“IO流\src\IO流\标准输出流\标准输出流自定义文档”));
第二步、改变位置,从控制台到 System.setOut(printStream1);
//直接使用再输出到自定义文档当中
printStream1.println(1);
printStream1.println(2);
printStream1.println(3);

package IO流.Print标准输出专属流;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;

public class PrintStream{
    public static void main(String[] args) throws FileNotFoundException {
        //分开写
        PrintStream printStream = System.out; //默认是打印写出到控制台System.out
        printStream.println(123);
        printStream.println("我喜欢你");
        printStream.println("abc");
        //联合写
        System.out.println("你还好么");//其实这个System也是PrintStream



        //可以改变或者指定,终端信息打印位置(默认是打印到控制台 System.out)
        //此时不再指向控制台,指向的是new新对象FileOutputStream的文本文件
        PrintStream printStream1 = new PrintStream(new FileOutputStream("IO流\\src\\IO流\\标准输出流\\标准输出流自定义文档"));
        System.setOut(printStream1);
        //直接使用再输出到自定义文档当中,两种方式分开写和一起写。
        printStream1.println(1);
        printStream1.println(2);
        printStream1.println(3);
        System.out.println(1);
        System.out.println(2);
        System.out.println(3);



        //最后不用写关闭方法,系统会会自动关闭
    }
}

案例:记录日志(什么时间+干了什么事务)的方法。要append追加

package IO流.Print标准输出专属流;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class printStream流进阶记录日志 {
    public static void main(String[] args) {
        Log.log("调用了一个菜单接口");
        Log.log("调用了System.gc()垃圾回收器方法");
        Log.log("调用了IO流FileOutputStream,写入了文件");
        Log.log("有用户尝试登陆,验证失败");

    }
}


class Log{
    //记录日志(什么时间+干了什么事务)的方法。要append追加
    public static void log(String string ){
        try {
            PrintStream printStream = new PrintStream(new FileOutputStream("IO流\\src\\IO流\\Print标准输出专属流\\log日志",true));
            //即流套流产生联系,使得包装流可以以文件输出的形式输出或输入)
            //要放追加,因为调用一次只写入一次就结束了。因此要追加。同一方法里面写才不用追加
            System.setOut(printStream);

            Date NowTime = new Date();//创建当前时间日期
            SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");//修改日期格式
            String str = s.format(NowTime);
            System.out.println(str+":"+ string); //string方法形参


        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
2021-01-05 15:26:12 374:有用户尝试登陆,验证失败
2021-01-05 15:26:35 274:调用了一个菜单接口
2021-01-05 15:26:35 347:调用了System.gc()垃圾回收器方法
2021-01-05 15:26:35 348:调用了IO流FileOutputStream,写入了文件
2021-01-05 15:26:35 349:有用户尝试登陆,验证失败

十、序列化和反序列化Object专属流:
一、对象Object专属流:
ObjectInputStream、ObjectOutputStream
(序列化写出对象,反序列化就是读取对象,上面那些是写出和读取文件类型图片,bgm,基本数据等)

二、序列化就是加密去写出对象(商业机密使用,一样不能随便看写出的文件是加密的)
序列化:就是写出对象—> ObjectOutputStream
反序列化:就是读取对象---->ObjectInputStream

三:序列化和反序列化注意事项:
1、反序列化读取之前,一直要先序列化写出对象(这个过程会new对象),因为对象自己在文档手动写不出!
2、反序列化读取每次只能执行一次,想重复执行反序列化必须先执行序列化先。
3、不管是系列化类还是集合,读取对象时,返回值写Object o万金油即可。
4、所有参与序列化和反序列化的对象类,必须重写好toString方法
5、所有参与序列化和反序列化的对象类,必须去implements实现Serializable这个序列化标志性接口。否则报错!!!
6、接口分为两类,一种是专门实现方法的,一种是标志性接口(让jvm识别你要去序列化,帮你生成序列化版本号,区分类)
至于什么是序列化版本号??

先看看一个异常:
Exception in thread “main” java.io.InvalidClassException: 序列化和反序列化Object专属流.单个类对象.Student; local class incompatible:stream classdesc serialVersionUID = -9026121290498725668,
(未修改类之前的版本序列号,此时Student类未加int age)
local class serialVersionUID = 1637514239343387366。
(修改类后的版本序列号,此时Student类加了int age后)

①这个问题 这是在反序列化时读取对象类时发生的问题,因为在反序列化读取之前自己偷偷修改了Student类,然后又没有去再次序列化写出更新旧类内容到文件中,而是直接再次去反序列化读取,因此jvm就判定,你不对劲,你们两个类的内容已经不一致了,版本号也不一致了,直接报错。

②因此总结: 两次的Student类虽然是同一个类名,但是内容已经不一致发生了变化,
jvm已经判断你们不是一个相同的类,除非重新执行反序列化(写出),然后反序列化(再次读取)。否则直接读取会报错直接异常,因为内容不一致,没有更新写出到文件中。

2、因此实现Serialize接口有什么用???
(实现Serialize接口就会有自动的序列化版本号,给jvm区分类用的)
【1】优点:只要是实现了Serialize接口,就都会去帮你区分两个类。
1、看类名是否相同。
2、类名相同下,就会去看序列化版本号,因为每个类都有独有的版本号。

【2】缺点:
不能再去修改类中的代码,一旦修改了类中的内容,反序列化读取时就又会默认生成新的版本号,当成是新的类,然后版本号不一致报错。(旧的类的版本号就废了)
例如上面:虽然是同一个Student类名,但是内容已经发生了改变。(多加了int age)
又因为这个类实现了Serialize接口,有独有的序列化版本号,因此内容改变时,序列化写出时版本号就会改变另外生成新的,
java虚拟机就会认为这个Student类修改前和修改后两个是不同的类。

3、如何解决修改类后,没有及时再次序列化写出更新原来的类,反序列化读取就会当成是两个不同类的报错问题???
①要不就不要修改代码
②一旦如果修改了,就去重新再去执行一次去序列化写出新版本号对应的更新类对象,然后再去反序列化读出对象。(太麻烦)
③给实现了Serialize接口的类提供一个永久不变的序列化版本号。(固定死,怎么修改都当成原有的类,都是同一个版本号,完事大吉)
手动打: public static final long serialVersionUID = 123456L;
idea自动:private static final long serialVersionUID = -2978470818292104897L; alt+回车类

四、transient关键字???(放属性声明前面)
使用该关键字之后,表示该属性不参与序列化操作,即这个带有transient关键字的对象属性不参与写出或者读取输出。(序列化写出完全不参与,反序列化读取会返回null)

序列化单个对象实现(写出对象):

package 序列化和反序列化Object专属流.单个类对象;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class 序列化实现 {
    public static void main(String[] args) throws Exception {
        //1、得先有,即创建对象才能写出吧
        Student s1 = new Student(123,"涂岳新");
       //2、将对象写出去:序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("IO流\\src\\序列化和反序列化Object专属流\\对象专属流自定义文档1"));
        //"main" java.io.NotSerializableException: 序列化和反序列化.Student
        //但是会直接报出异常?????
        //原因:Student这个类不支持序列化,因此类对象要去implements实现Serializable这个接口。

       //开始序列化对象
        out.writeObject(s1);


        out.flush();
        out.close();
    }

}
class Student implements Serializable {
    private static final long serialVersionUID = -2978470818292104897L;
    //    public static final long serialVersionUID = 123456L;
    private String name;
    private int no;


    public Student(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

反序列化单个对象的实现(读取对象):

package 序列化和反序列化Object专属流.单个类对象;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
/*
1、反序列化就是:把硬盘的java对象恢复到内存中去,恢复成原来的java对象。(等于读取ObjectInputStream)
2、反序列化即ObjectInputStream,也就是读取这个存有对象的文件

*/
public class 反序列化实现 {
    public static void main(String[] args) throws Exception {

        //创建反序列化流,即 ObjectInputStream
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("IO流\\src\\序列化和反序列化Object专属流\\对象专属流自定义文档1"));

        //开始反序列化对象。
        Object o = in.readObject();
        System.out.println(o.toString());//类一定要重写好toString方法

        in.close();
    }
}

序列化多个对象实现(写出多个对象到文件):
可以序列化多个对象么????、
可以的,但是要使用集合将两个引用对象类型,放一起去序列化集合对象。
1、可以序列化同一个类的多个实例对象 用ArrayList
2、也可以序列化多个不同的类的类对象 用hashMap

public class 序列化多个对象 {
    public static void main(String[] args) throws Exception{
        /* Student1 s1 = new Student1(111222333,"lry");
           User u1 = new User("lry",12,123456);
*/
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("IO流\\src\\序列化和反序列化Object专属流\\对象专属流自定义文档2"));

        HashMap<Student1,User> hashMap = new HashMap<>();
        hashMap.put(new Student1(111222333,"lry"),new User("lry",12,123456));
        hashMap.put(new Student1(123,"tuyueixn"),new User("lisi",13,1234567));

        //序列化集合对象,等于间接序列化多个类对象
        out.writeObject(hashMap);

        out.flush();
        out.close();

    }
}


class Student1 implements Serializable {
    private int no;
    private String name;

    public Student1(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

class User implements Serializable{
    String name;
    int age;
    int no;

    public User(String name, int age, int no) {
        this.name = name;
        this.age = age;
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", no=" + no +
                '}';
    }
}

反序列化多个对象(读取文件对象):

public class 反序列化多个对象 {
    public static void main(String[] args) throws Exception{
        //创建反序列化流,即 ObjectInputStream
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("IO流\\src\\序列化和反序列化Object专属流\\对象专属流自定义文档2"));

        //开始反序列化读取对象。
//        Object  o = in.readObject();  但是Object需要强制转为hashmap集合

        HashMap<Student1,User> hashMap1 =  (HashMap<Student1,User>)in.readObject();
        Set set = hashMap1.entrySet();
        for (Object o :set){
            System.out.println(o);
        }

        //图方便直接这样也行,直接输出,但只会输出一行。
       /* Object  o = in.readObject();
        System.out.println(o.toString());//类一定要重写好toString方法*/

        in.close();
    }
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值