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方法执行时,会进入一个if、else语句,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方法里面是一个do,while循环,拿当前存进去的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、例如 可以用比较类1:new CompareToStudent1() 。就放Map<Student1,Integer> m=new TreeMap<>(new CompareToStudent1());
也可以用比较类2:new 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();
}
}