集合

集合概述

1、什么是集合?有什么用?
数组其实就是一个集合。集合实际上就是一个容器,可以来容纳其他类型的数据。

集合为什么说在开发中使用较多?
集合是一个容器,是一个载体,可以一次容纳多个对象。
在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传给前端,然后遍历集合,将一个数据一个数据展现出来。

2、集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用)
list.add(100); //自动装箱Integer
注意:
集合在java中本身是一个容器,是一个对象。
集合中任何时候存储的都是“引用”。

3、在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构当中。什么是数据结构?数据存储的结构就是数据结构。不同的数据结构,数据的存储方式不同。例如:
数组、二叉树、链表、哈希表…
以上这些都是常见的数据结构。

4、集合在java JDK中哪个包下?
java.util.*;
所有的集合类和集合接口都在java.util包下。

5、在java中集合分为两大类:
一类是单个方式存储元素:
单个方式存储元素,这一类集合中超级父接口:java.util.Collection;

一类是以键值对的方式存储元素:
以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map;

集合的继承结构图

Collection:

Map:

总结

  • ArrayList:底层是数组。
  • LinkedList:底层是双向链表。
  • Vector:底层是数组,线程安全的,效率较低,使用较少。
  • HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合key部分了。
  • TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了。
  • HashMap:底层是哈希表。
  • Hashtable:底层也是哈希表,只不过线程安全的,效率较低,使用较少。
  • Properties:是线程安全的,并且key和value只能存储字符串String。
  • TreeMap:底层是二叉树。TreeMap集合的key可以自动按照大小顺序排序。

List集合存储元素的特点:
有序可重复
有序:存进去的顺序和取出的顺序相同,每一个元素都有下标。
可重复:存进去1,可以再存储一个1.

Set集合存储元素的特点:
无序不可重复
无序:存进去的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标。
不可重复:存进去1,不能再存储1了。

SortedSet(SortedMap)集合存储元素特点:
首先是无序不可重复的,但是SortedSet集合中的元素是可排序的。
无序:存进去的顺序和取出的顺序不一定相同。另外Set集合中元素没有下标。
不可重复:存进去1,不能再存储1了。
可排序:可以按照大小顺序排列。

Map集合的key,就是一个Set集合。
在Set集合中放数据,实际上放到了Map集合的key部分。

java.util.Collection接口

Collection中能存放什么元素?

没有使用“泛型”之前,Collection中可以存储Object的所有子类型。
使用了“泛型”之后,Collection中只能存储某个具体的类型。
集合中不能直接存储基本数据类型,也不能存java对象,只是存储java对象的内存地址。

常用方法

**boolean add(Object e) **:向集合中添加元素
**int size() **:获取集合中元素的个数
void clear() :清空集合
boolean contains(Object o) :判断当前集合中是否包含元素o,包含返回true,不包含返回false
**boolean remove(Object o) **:删除集合中的某个元素
**boolean isEmpty() **:判断该集合中元素的个数是否为0
Object[] toArray() :调用这个方法可以把集合转换成数组(作为了解,使用不多)

关于集合遍历/迭代

注意:
以下学的遍历方式/迭代方式,是所有Collection通用的一种方式。
在Map集合中不能用。在所有的Collection以及子类中使用。

对集合Collection进行遍历/迭代:
第一步:获取集合对象的迭代器对象Iterator
第二步:通过以上获取的迭代器对象开始迭代/遍历集合
以下两个方法是迭代器对象Iterator中的方法:
**boolean hasNext() **如果仍有元素可以迭代,则返回true。
**Object next() **返回迭代的下一个元素。

如果一直取,不判断,会出现异常:java.util.NoSuchElementException

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;public class CollectionTest02 {public static void main(String[] args) {// 注意:以下讲解的遍历方式/迭代方式,是所有Collection通用的一种方式。// 在Map集合中不能用。在所有的Collection以及子类中使用。// 创建集合对象Collection c = new ArrayList(); // 后面的集合无所谓,主要是看前面的Collection接口,怎么遍历/迭代。// 添加元素c.add("abc");c.add("def");c.add(100);c.add(new Object());// 对集合Collection进行遍历/迭代// 第一步:获取集合对象的迭代器对象IteratorIterator it = c.iterator();// 第二步:通过以上获取的迭代器对象开始迭代/遍历集合。/*以下两个方法是迭代器对象Iterator中的方法:boolean hasNext()如果仍有元素可以迭代,则返回 true。Object next() 返回迭代的下一个元素。*/while(it.hasNext()){Object obj = it.next();System.out.println(obj);}}
}
public class CollectionTest03 {public static void main(String[] args) {// 创建集合对象Collection c1  = new ArrayList(); // ArrayList集合:有序可重复// 添加元素c1.add(1);c1.add(2);c1.add(3);c1.add(4);c1.add(1);// 迭代集合Iterator it = c1.iterator();while(it.hasNext()){// 存进去是什么类型,取出来还是什么类型。Object obj = it.next();/*if(obj instanceof Integer){System.out.println("Integer类型");}*/// 只不过在输出的时候会转换成字符串。因为这里println会调用toString()方法。System.out.println(obj);}// HashSet集合:无序不可重复Collection c2 = new HashSet();// 无序:存进去和取出的顺序不一定相同。// 不可重复:存储100,不能再存储100.c2.add(100);c2.add(200);c2.add(300);c2.add(90);c2.add(400);c2.add(50);c2.add(60);c2.add(100);Iterator it2 = c2.iterator();while(it2.hasNext()){System.out.println(it2.next());}}
}

contains方法源码分析

boolean contains(Object o)
判断集合中是否包含某个对象o
如果包含返回true,如果不包含返回false。

contains方法是用来判断集合中是否包含某个元素的方法,那么它在底层是怎么判断集合中是否包含某个元素的呢?
调用了equals方法进行比对。equals方法返回true,就表示包含这个元素。

import java.util.ArrayList;
import java.util.Collection;public class CollectionTest04 {public static void main(String[] args) {// 创建集合对象Collection c = new ArrayList();// 向集合中存储元素String s1 = new String("abc"); // s1 = 0x1111c.add(s1); // 放进去了一个"abc"String s2 = new String("def"); // s2 = 0x2222c.add(s2);// 集合中元素的个数System.out.println("元素的个数是:" + c.size()); // 2// 新建的对象StringString x = new String("abc"); // x = 0x5555// c集合中是否包含x?结果猜测一下是true还是false?System.out.println(c.contains(x)); //判断集合中是否存在"abc" true}
}
import java.util.ArrayList;
import java.util.Collection;public class CollectionTest05 {public static void main(String[] args) {// 创建集合对象Collection c = new ArrayList();// 创建用户对象User u1 = new User("jack");// 加入集合c.add(u1);// 判断集合中是否包含u2User u2 = new User("jack");// 没有重写equals之前:这个结果是false//System.out.println(c.contains(u2)); // false// 重写equals方法之后,比较的时候会比较name。System.out.println(c.contains(u2)); // true/*Integer x = new Integer(10000);c.add(x);Integer y = new Integer(10000);System.out.println(c.contains(y)); // true*/}
}class User{private String name;public User(){}public User(String name){this.name = name;}// 重写equals方法// 将来调用equals方法的时候,一定是调用这个重写的equals方法。// 这个equals方法的比较原理是:只要姓名一样就表示同一个用户。public boolean equals(Object o) {if(o == null || !(o instanceof User)) return false;if(o == this) return true;User u = (User)o;// 如果名字一样表示同一个人。(不再比较对象的内存地址了。比较内容。)return u.name.equals(this.name);}
}

结论:存放在一个集合中的类型,一定要重写equals方法。

remove方法源码分析

remove底层也是调用equals方法进行对比。

// 创建集合对象
Collection cc = new ArrayList();
// 创建字符串对象
String s1 = new String("hello");
// 加进去。
cc.add(s1);// 创建了一个新的字符串对象
String s2 = new String("hello");
// 删除s2
cc.remove(s2); // s1.equals(s2) java认为s1和s2是一样的。删除s2就是删除s1。
// 集合中元素个数是?
System.out.println(cc.size()); // 0

重点:当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现异常:java.util.ConcurrentModificationException

重点:在迭代集合元素的过程中,不能调用集合对象的remove方法删除元素:
c.remove(o); 迭代过程中不能这样。
会出现:java.util.ConcurrentModificationException
原因:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了),导致迭代器的快照和原集合状态不同。

重点:在迭代元素的过程当中,一定要使用迭代器Iterator的remove方法删除元素,不要使用集合自带的remove方法删除元素。迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;public class CollectionTest06 {public static void main(String[] args) {// 创建集合Collection c = new ArrayList();// 注意:此时获取的迭代器,指向的是那是集合中没有元素状态下的迭代器。// 一定要注意:集合结构只要发生改变,迭代器必须重新获取。// 当集合结构发生了改变,迭代器没有重新获取时,调用next()方法时:java.util.ConcurrentModificationExceptionIterator it = c.iterator();// 添加元素c.add(1); // Integer类型c.add(2);c.add(3);// 获取迭代器//Iterator it = c.iterator();/*while(it.hasNext()){// 编写代码时next()方法返回值类型必须是Object。// Integer i = it.next();Object obj = it.next();System.out.println(obj);}*/Collection c2 = new ArrayList();c2.add("abc");c2.add("def");c2.add("xyz");Iterator it2 = c2.iterator();while(it2.hasNext()){Object o = it2.next();// 删除元素// 删除元素之后,集合的结构发生了变化,应该重新去获取迭代器// 但是,循环下一次的时候并没有重新获取迭代器,所以会出现异常:java.util.ConcurrentModificationException// 出异常根本原因是:集合中元素删除了,但是没有更新迭代器(迭代器不知道集合变化了)//c2.remove(o); // 直接通过集合去删除元素,没有通知迭代器。(导致迭代器的快照和原集合状态不同。)// 使用迭代器来删除可以吗?// 迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中的元素)。it2.remove(); // 删除的一定是迭代器指向的当前元素。System.out.println(o);}System.out.println(c2.size()); //0}
}

java.util.List接口

List集合存储元素的特点:有序可重复
有序:List集合中的元素有下标。
从0开始,以1递增。
可重复:存储一个1,还可以再存储1.

List既然是Collection接口的子接口,那么肯定List接口有自己“特色”的方法:
以下只列出List接口特有的常用的方法:

常用方法

**void add(int index, Object element) **:在列表的指定位置插入指定元素(使用不多,因为效率比较低)
Object set(int index, Object element) :修改指定位置的元素
Object get(int index) :根据下标获取元素
int indexOf(Object o) :获取指定对象第一次出现处的索引
int lastIndexOf(Object o) :获取指定对象最后一次出现处的索引
Object remove(int index) :删除指定下标位置的元素

List集合有自己特色的遍历:

for(int i = 0; i < myList.size(); i++){Object obj = myList.get(i);System.out.println(obj);
}

ArrayList集合

1、默认初始化容量10(底层先创建了一个长度为0的数组,当添加第一个元素的时候,初始化容量10。)
2、集合底层是一个Object[]数组。
3、构造方法是:
new ArrayList();
new ArrayList(int);
new ArrayList(Collection<? extends E>);
4、ArrayList集合的扩容:
增长到原容量的1.5倍
ArrayList集合底层是数组,怎么优化?
尽可能少的扩容。因为数组扩容效率比较低,建议在使用ArrayList集合的时候预估计元素的个数,给定一个初始化容量。
5、数组优点:
检索效率比较高。(每个元素占用空间大小相同,内存地址是连续的,知道首元素内存地址,然后知道下标,通过数学表达式计算出元素的内存地址,所以检索效率最高。)
6、数组缺点:
随机增删元素效率比较低。
另外数组无法存储大数据量。(很难找到一块非常巨大的连续的内存空间。)
7、向数组末尾添加元素,效率很高,不受影响。
8、面试官经常问的一个问题?
这么多的集合中,你用哪个集合最多?
答:ArrayList集合。
因为往数组末尾添加元素,效率不受影响。
另外,我们检索/查找某个元素的操作比较多。
9、ArrayList集合是非线程安全的。(不是线程安全的集合。)

单向链表数据结构

单链表中的节点:
节点是单向链表中的基本的单元。
每一个节点Node都有两个属性:
一个属性:是存储的数据。
另一个属性:是下一个节点的内存地址。

public class Node {// 存储的数据Object data;// 下一个节点的内存地址Node next;public Node(){}public Node(Object data, Node next){this.data = data;this.next = next;}
}

单链表类:

public class Link<E> {public static void main(String[] args) {Link<String> link = new Link<>();link.add("abc");// 类型不匹配。//link.add(123);}// 头节点Node header;int size = 0;public int size(){return size;}// 向链表中添加元素的方法(向末尾添加)public void add(E data){//public void add(Object data){// 创建一个新的节点对象// 让之前单链表的末尾节点next指向新节点对象。// 有可能这个元素是第一个,也可能是第二个,也可能是第三个。if(header == null){// 说明还没有节点。// new一个新的节点对象,作为头节点对象。// 这个时候的头节点既是一个头节点,又是一个末尾节点。header = new Node(data, null);}else {// 说明头不是空!// 头节点已经存在了!// 找出当前末尾节点,让当前末尾节点的next是新节点。Node currentLastNode = findLast(header);currentLastNode.next = new Node(data, null);}size++;}/*** 专门查找末尾节点的方法。*/private Node findLast(Node node) {if(node.next == null) {// 如果一个节点的next是null// 说明这个节点就是末尾节点。return node;}// 程序能够到这里说明:node不是末尾节点。return findLast(node.next); // 递归算法!}// 删除链表中某个数据的方法public void remove(Object obj){}// 修改链表中某个数据的方法public void modify(Object newObj){}// 查找链表中某个元素的方法。public int find(Object obj){return 1;}
}

链表的优点

由于链表上的元素在空间存储上内存地址不连续。
所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高。在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议使用LinkedList。

链表的缺点

不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头节点开始遍历,直到找到为止。所以LinkedList集合检索/查找的效率较低。

双向链表数据结构

LinkedList集合

ArrayList:把检索发挥到极致。(末尾添加元素效率还是很高的。)
LinkedList:把随机增删发挥到极致。
加元素都是往末尾加,所以ArrayList用的比LinkedList多。

LinkedList集合底层也是有下标的。(其实下标就是一个虚的东西,只不过是存的时候是有序的,所以就说是有下标的)
注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因,是因为底层数组发挥的作用。
LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历。

LinkedList集合有初始化容量吗?没有
最初这个链表中没有任何元素。first和last引用都是null。

不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。
因为我们要面向接口编程,调用的方法都是接口中的方法。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class LinkedListTest01 {public static void main(String[] args) {// LinkedList集合底层也是有下标的。// 注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因。是因为底层数组发挥的作用。// LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从头节点开始一个一个遍历。List list = new LinkedList();list.add("a");list.add("b");list.add("c");for(int i = 0; i <list.size(); i++){Object obj = list.get(i);System.out.println(obj);}// LinkedList集合有初始化容量吗?没有。// 最初这个链表中没有任何元素。first和last引用都是null。// 不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合。// 因为我们要面向接口编程,调用的方法都是接口中的方法。//List list2 = new ArrayList(); // 这样写表示底层你用了数组。List list2 = new LinkedList(); // 这样写表示底层你用了双向链表。// 以下这些方法你面向的都是接口编程。list2.add("123");list2.add("456");list2.add("789");for(int i = 0; i < list2.size(); i++){System.out.println(list2.get(i));}}
}

Vector集合

1、底层也是一个数组。
2、初始化容量:10
3、怎么扩容的?
扩容之后是原容量的2倍

4、ArrayList集合扩容特点:
ArrayList集合扩容是原容量的1.5倍。

5、Vector中所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的。效率比较低,使用较少。

6、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
使用集合工具类:
java.util.Collections;

java.util.Collection 是集合接口。
java.util.Collections 是集合工具类。

import java.util.*;public class VectorTest {public static void main(String[] args) {// 创建一个Vector集合List vector = new Vector();//Vector vector = new Vector();// 添加元素// 默认容量10个。vector.add(1);vector.add(2);vector.add(3);vector.add(4);vector.add(5);vector.add(6);vector.add(7);vector.add(8);vector.add(9);vector.add(10);// 满了之后扩容(扩容之后的容量是20.)vector.add(11);Iterator it = vector.iterator();while(it.hasNext()){Object obj = it.next();System.out.println(obj);}// 这个可能以后要使用!!!!List myList = new ArrayList(); // 非线程安全的。// 变成线程安全的Collections.synchronizedList(myList); // 这里没有办法看效果,因为多线程没学,你记住先!// myList集合就是线程安全的了。myList.add("111");myList.add("222");myList.add("333");}
}

泛型机制

1、JDK5.0之后推出的新特性:泛型。
2、泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
3、使用了泛型好处是什么?
第一:集合中存储的元素类型统一了。
第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!

4、泛型的缺点是什么?
导致集合中存储的元素缺乏多样性!
大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class GenericTest01 {public static void main(String[] args) {// 使用JDK5之后的泛型机制// 使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。// 用泛型来指定集合中存储的数据类型。List<Animal> myList = new ArrayList<Animal>();// 指定List集合中只能存储Animal,那么存储String就编译报错了。// 这样用了泛型之后,集合中元素的数据类型更加统一了。//myList.add("abc");Cat c = new Cat();Bird b = new Bird();myList.add(c);myList.add(b);// 获取迭代器// 这个表示迭代器迭代的是Animal类型。Iterator<Animal> it = myList.iterator();while(it.hasNext()){// 使用泛型之后,每一次迭代返回的数据都是Animal类型。//Animal a = it.next();// 这里不需要进行强制类型转换了。直接调用。//a.move();// 调用子类型特有的方法还是需要向下转换的!Animal a = it.next();if(a instanceof Cat) {Cat x = (Cat)a;x.catchMouse();}if(a instanceof Bird) {Bird y = (Bird)a;y.fly();}}}
}class Animal {// 父类自带方法public void move(){System.out.println("动物在移动!");}
}class Cat extends Animal {// 特有方法public void catchMouse(){System.out.println("猫抓老鼠!");}
}class Bird extends Animal {// 特有方法public void fly(){System.out.println("鸟儿在飞翔!");}
}

自动类型推断机制(钻石表达式)

JDK8之后引入了:自动类型推断机制,又称为钻石表达式。<>

public class GenericTest02 {public static void main(String[] args) {// ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许。// 自动类型推断,钻石表达式!List<Animal> myList = new ArrayList<>();myList.add(new Animal());myList.add(new Cat());myList.add(new Bird());// 遍历Iterator<Animal> it = myList.iterator();while(it.hasNext()){Animal a = it.next();a.move();}List<String> strList = new ArrayList<>();// 类型不匹配。//strList.add(new Cat());strList.add("http://www.126.com");strList.add("http://www.baidu.com");strList.add("http://www.bjpowernode.com");// 类型不匹配。//strList.add(123);//System.out.println(strList.size());// 遍历Iterator<String> it2 = strList.iterator();while(it2.hasNext()){// 如果没有使用泛型/*Object obj = it2.next();if(obj instanceof String){String ss = (String)obj;ss.substring(7);}*/// 直接通过迭代器获取了String类型的数据String s = it2.next();// 直接调用String类的substring方法截取字符串。String newString = s.substring(7);System.out.println(newString);}}
}

自定义泛型

自定义泛型的时候,<>尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:

E是Element单词首字母
T是Type单词首字母

public class GenericTest03<标识符随便写> {public void doSome(标识符随便写 o){System.out.println(o);}public static void main(String[] args) {// new对象的时候指定了泛型是:String类型GenericTest03<String> gt = new GenericTest03<>();// 类型不匹配//gt.doSome(100);gt.doSome("abc");// =============================================================GenericTest03<Integer> gt2 = new GenericTest03<>();gt2.doSome(100);// 类型不匹配//gt2.doSome("abc");MyIterator<String> mi = new MyIterator<>();String s1 = mi.get();MyIterator<Animal> mi2 = new MyIterator<>();Animal a = mi2.get();// 不用泛型就是Object类型。/*GenericTest03 gt3 = new GenericTest03();gt3.doSome(new Object());*/}
}class MyIterator<T> {public T get(){return null;}
}

foreach

JDK5.0之后推出的新特性:叫做增强for循环,或者叫做foreach。

public class ForEachTest01 {public static void main(String[] args) {// int类型数组int[] arr = {432,4,65,46,54,76,54};// 遍历数组(普通for循环)for(int i = 0; i < arr.length; i++) {System.out.println(arr[i]);}// 增强for(foreach)// 以下是语法/*for(元素类型 变量名 : 数组或集合){System.out.println(变量名);}*/System.out.println("======================================");// foreach有一个缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。for(int data : arr) {// data就是数组中的元素(数组中的每一个元素。)System.out.println(data);}}
}

集合使用foreach:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class ForEachTest02 {public static void main(String[] args) {// 创建List集合List<String> strList = new ArrayList<>();// 添加元素strList.add("hello");strList.add("world!");strList.add("kitty!");// 遍历,使用迭代器方式Iterator<String> it = strList.iterator();while(it.hasNext()){String s = it.next();System.out.println(s);}// 使用下标方式(只针对于有下标的集合)for(int i = 0; i < strList.size(); i++){System.out.println(strList.get(i));}// 使用foreachfor(String s : strList){ // 因为泛型使用的是String类型,所以是:String sSystem.out.println(s);}List<Integer> list = new ArrayList<>();list.add(100);list.add(200);list.add(300);for(Integer i : list){ // i代表集合中的元素System.out.println(i);}}
}

java.util.Map接口

1、Map和Collection没有继承关系。
2、Map集合以key和value的方式存储数据:键值对
key和value都是引用数据类型。
key和value都是存储对象的内存地址。
key起到主导的地位,value是key的一个附属品。

常用方法

V put(K key, V value) :向Map集合中添加键值对
V get(Object key) :通过key获取value
void clear() :清空Map集合
boolean containsKey(Object key) :判断Map中是否包含某个key
boolean containsValue(Object value) :判断Map中是否包含某个value
boolean isEmpty() :判断Map集合中元素个数是否为0
V remove(Object key) :通过key删除键值对
int size() :获取Map集合中键值对的个数
Collection values() :获取Map集合中所有的value,返回一个Collection
Set keySet() :获取Map集合所有的key(所有的键是一个set集合)
Set<Map.Entry<K, V>> entrySet() :将Map集合转换成Set集合

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;public class MapTest01 {public static void main(String[] args) {// 创建Map集合对象Map<Integer, String> map = new HashMap<>();// 向Map集合中添加键值对map.put(1, "zhangsan"); // 1在这里进行了自动装箱。map.put(2, "lisi");map.put(3, "wangwu");map.put(4, "zhaoliu");// 通过key获取valueString value = map.get(2);System.out.println(value);// 获取键值对的数量System.out.println("键值对的数量:" + map.size());// 通过key删除key-valuemap.remove(2);System.out.println("键值对的数量:" + map.size());// 判断是否包含某个key// contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。System.out.println(map.containsKey(new Integer(4))); // true// 判断是否包含某个valueSystem.out.println(map.containsValue(new String("wangwu"))); // true// 获取所有的valueCollection<String> values = map.values();// foreachfor(String s : values){System.out.println(s);}// 清空map集合map.clear();System.out.println("键值对的数量:" + map.size());// 判断是否为空System.out.println(map.isEmpty()); // true}
}

Map集合的遍历

两种方式
第一种涉及的方法有:keySet()和get(Object key)
第二种涉及的方法有:entrySet()

public class MapTest02 {public static void main(String[] args) {// 第一种方式:获取所有的key,通过遍历key,来遍历valueMap<Integer, String> map = new HashMap<>();map.put(1, "zhangsan");map.put(2, "lisi");map.put(3, "wangwu");map.put(4, "zhaoliu");// 遍历Map集合// 获取所有的key,所有的key是一个Set集合Set<Integer> keys = map.keySet();// 遍历key,通过key获取value// 迭代器可以/*Iterator<Integer> it = keys.iterator();while(it.hasNext()){// 取出其中一个keyInteger key = it.next();// 通过key获取valueString value = map.get(key);System.out.println(key + "=" + value);}*/// foreach也可以for(Integer key : keys){System.out.println(key + "=" + map.get(key));}// 第二种方式:Set<Map.Entry<K,V>> entrySet()// 以上这个方法是把Map集合直接全部转换成Set集合。// Set集合中元素的类型是:Map.EntrySet<Map.Entry<Integer,String>> set = map.entrySet();// 遍历Set集合,每一次取出一个Node// 迭代器/*Iterator<Map.Entry<Integer,String>> it2 = set.iterator();while(it2.hasNext()){Map.Entry<Integer,String> node = it2.next();Integer key = node.getKey();String value = node.getValue();System.out.println(key + "=" + value);}*/// foreach// 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。// 这种方式比较适合于大数据量。for(Map.Entry<Integer,String> node : set){System.out.println(node.getKey() + "--->" + node.getValue());}}
}

哈希表数据结构

1、HashMap集合底层是哈希表/散列表的数据结构。
2、哈希表是一个怎样的数据结构呢?
哈希表是一个数组和单向链表的结合体。
数组:在查询方面效率很高,随机增删方面效率很低。
单向链表:在随机增删方面效率较高,在查询方面效率很低。
哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。

HashMap集合

HashMap集合底层的源代码:

public class HashMap{// HashMap底层实际上就是一个数组。(一维数组)Node<K,V>[] table;// 静态的内部类HashMap.Nodestatic class Node<K,V> implements Map.Entry<K,V>{final int hash; //哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函   数/算法,可以转换存储成数组的下标。)final K key; // 存储到Map集合中的那个keyV value; // 存储到Map集合中的那个valueNode<K,V> next; //下一个节点的内存地址}
}

哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)

HashMap集合的默认初始化容量是16,默认加载因子(DEFAULT_LOAD_FACTOR)是0.75f。扩容之后的容量是原容量的2倍
这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。

重点,记住:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率所必须的。

主要掌握

map.put(k,v)
v = map.get(k)
以上两个方法的实现原理必须掌握。

map.put(k,v)实现原理
第一步:先将k,v封装到Node对象当中。
第二步:底层会调用k的hashCode()方法得出hash值,然后通过哈希函数/算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上了。如果说下标对应的位置上有链表,此时会拿着k和链表上每一个节点中的k进行equals,如果所有的equals方法返回都是false,那么这个新节点将会被添加到链表的末尾。如果其中有一个equals返回了true,那么这个节点的value将会被覆盖。

v = map.get(k)实现原理
先调用k的hashCode()方法得出hash值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上,如果这个位置上什么也没有,返回null。如果这个位置上有单向链表,那么会拿着参数k和单向链表上的每个节点中的k进行equals,如果所有equals方法返回false,那么get方法返回null,只要其中有一个节点的k和参数k equals的时候返回true,那么此时这个节点的value就是我们要找的value,get方法最终返回这个要找的value。

为什么哈希表的随机增删,以及查询效率都很高?
增删是在链表上完成。
查询也不需要都扫描,只需要部分扫描。

重点:通过以上可以得出HashMap集合的key,会先后调用两个方法,一个方法是hashCode(),一个方法是equals(),那么这两个方法都需要重写

注意:同一个单向链表上所有节点的hash相同,因为它们的数组下标是一样的。但同一个链表上k和k的equals方法肯定返回的是false,都不相等。

key部分的特点

HashMap集合的key部分的特点:
无序,不可重复。
为什么无序?因为不一定挂到哪个单向链表上。
不可重复是怎么保证的?equals方法来保证HashMap集合的key不可重复。
如果key重复了,value会覆盖。

放在HashMap集合key部分的元素其实就是放到HashSet集合中了。
所以HashSet集合中的元素也需要同时重写hashCode()+equals()方法。

哈希表HashMap使用不当时无法发挥效能!
假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们称为:散列分布不均匀。
什么是散列分布均匀?
假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,是散列分布均匀的。
假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗?
不行,因为这样的话导致底层哈希表就称为一维数组了,没有链表的概念了。也是散列分布不均匀。
散列分布均匀需要重写hashCode()方法时有一定的技巧。

重点:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。

同时重写hashCode和equals

1、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
拿put(k,v)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标,数组下标位置上如果是null,equals不需要执行。
拿get(k)举例,什么时候equals不会调用?
k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标,数组下标位置上如果是null,equals不需要执行。

2、注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。
并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
equals方法返回true表示两个对象相同。在同一个单向链表上比较,那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。所以haseCode()方法的返回值也应该相同。

3、hashCode()方法和equals()方法不用研究,直接使用IDEA工具生成,但是这两个方法需要同时生成。

4、终极结论:
放在HashMap集合key部分,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法

5、对于哈希表数据结构来说:
如果o1和o2的hash值相同,一定是放到同一个单向链表上。
当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。

JDK8.0对HashMap集合的改进

在JDK8之后,如果哈希表单向链表中元素超过8个,单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。
这种方式也是为了提高检索效率,二叉树的检索会再次缩小扫描范围。提高效率。
static final int _TREEIFY_THRESHOLD _= 8;
static final int _UNTREEIFY_THRESHOLD _= 6;

HashMap集合key部分允许null吗?
允许。但是要注意:HashMap集合的key null值只能有一个。

public class HashMapTest03 {public static void main(String[] args) {Map map = new HashMap();// HashMap集合允许key为nullmap.put(null, null);System.out.println(map.size()); // 1// key重复的话value是覆盖!map.put(null, 100);System.out.println(map.size()); //1// 通过key获取valueSystem.out.println(map.get(null)); // 100}
}

HashMap和Hashtable的区别

Hashtable的key可以为null吗?
Hashtable的key和value都是不能为null的。(会报空指针异常)
HashMap集合的key和value都是可以为null的。

Hasbtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理导致效率较低,使用较少了。

Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量的2倍+1

Properties属性类

目前只需要掌握Properties属性类对象的相关方法即可。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。

import java.util.Properties;public class PropertiesTest01 {public static void main(String[] args) {// 创建一个Properties对象Properties pro = new Properties();// 需要掌握Properties的两个方法,一个存,一个取。pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");pro.setProperty("driver","com.mysql.jdbc.Driver");pro.setProperty("username", "root");pro.setProperty("password", "123");// 通过key获取valueString url = pro.getProperty("url");String driver = pro.getProperty("driver");String username = pro.getProperty("username");String password = pro.getProperty("password");System.out.println(url);System.out.println(driver);System.out.println(username);System.out.println(password);}
}

HashSet集合

TreeSet对String是可排序的

TreeSet集合底层实际上是一个TreeMap。
TreeMap集合底层是一个二叉树。
放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。
称为:可排序集合。

TreeSet无法对自定义类型排序

对自定义的类型来说,TreeSet可以排序吗?
以下程序中对于Person类型来说,无法排序。因为没有指定Person对象之间的比较规则
谁大谁小并没有说明。

以下程序运行的时候出现了这个异常:
java.lang.ClassCastException: com.bjpowernode.javase.collection.Person cannot be cast to java.lang.Comparable
出现这个异常的原因是:
Person类没有实现java.lang.Comparable接口。

import java.util.TreeSet;public class TreeSetTest03 {public static void main(String[] args) {Person p1 = new Person(32);//System.out.println(p1);Person p2 = new Person(20);Person p3 = new Person(30);Person p4 = new Person(25);// 创建TreeSet集合TreeSet<Person> persons = new TreeSet<>();// 添加元素persons.add(p1);persons.add(p2);persons.add(p3);persons.add(p4);// 遍历for (Person p : persons){System.out.println(p);}}
}class Person {int age;public Person(int age){this.age = age;}// 重写toString()方法public String toString(){return "Person[age="+age+"]";}
}

自定义类型实现Comparable接口

TreeSet集合中元素可排序的第一种方式:
放在TreeSet集合中的元素需要实现java.lang.Comparable接口。并且实现compareTo方法。equals可以不写。

import java.util.TreeSet;public class TreeSetTest04 {public static void main(String[] args) {Customer c1 = new Customer(32);Customer c2 = new Customer(20);Customer c3 = new Customer(30);Customer c4 = new Customer(25);// 创建TreeSet集合TreeSet<Customer> customers = new TreeSet<>();// 添加元素customers.add(c1);customers.add(c2);customers.add(c3);customers.add(c4);// 遍历for (Customer c : customers){System.out.println(c);}}
}// 放在TreeSet集合中的元素需要实现java.lang.Comparable接口。
// 并且实现compareTo方法。equals可以不写。
class Customer implements Comparable<Customer>{int age;public Customer(int age){this.age = age;}// 需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!// k.compareTo(t.key)// 拿着参数k和集合中的每一个k进行比较,返回值可能是>0 <0 =0// 比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。@Overridepublic int compareTo(Customer c) { // c1.compareTo(c2);// this是c1// c是c2// c1和c2比较的时候,就是this和c比较。/*int age1 = this.age;int age2 = c.age;if(age1 == age2){return 0;} else if(age1 > age2) {return 1;} else {return -1;}*///return this.age - c.age; // =0 >0 <0return c.age - this.age;}public String toString(){return "Customer[age="+age+"]";}
}

比较规则该怎么写

例子:

import java.util.TreeSet;/*
先按照年龄升序,如果年龄一样的再按照姓名升序。*/
public class TreeSetTest05 {public static void main(String[] args) {TreeSet<Vip> vips = new TreeSet<>();vips.add(new Vip("zhangsi", 20));vips.add(new Vip("zhangsan", 20));vips.add(new Vip("king", 18));vips.add(new Vip("soft", 17));for(Vip vip : vips){System.out.println(vip);}}
}class Vip implements Comparable<Vip>{String name;int age;public Vip(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Vip{" +"name='" + name + '\'' +", age=" + age +'}';}/*compareTo方法的返回值很重要:返回0表示相同,value会覆盖。返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】返回<0,会继续在左子树上找。*/@Overridepublic int compareTo(Vip v) {// 写排序规则,按照什么进行比较。if(this.age == v.age){// 年龄相同时按照名字排序。// 姓名是String类型,可以直接比。调用compareTo来完成比较。return this.name.compareTo(v.name);} else {// 年龄不一样return this.age - v.age;}}
}

自平衡二叉树数据结构

TreeSet/TreeMap是自平衡二叉树。遵循左小右大原则存放。
遍历二叉树的时候有三种方式:
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根

注意:
前中后说的是“根”的位置。
根在前面是前序,根在中间是中序,根在后面是后序。

TreeSet集合/TreeMap集合采用的是:中序遍历方式。左根右。

实现比较器接口

TreeSer集合中元素可排序的第二种方式:使用比较器的方式。
编写比较器可以改变规则,例如采用降序排序。

import java.util.Comparator;
import java.util.TreeSet;public class TreeSetTest06 {public static void main(String[] args) {// 创建TreeSet集合的时候,需要使用这个比较器。// TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去。// 给构造方法传递一个比较器。//TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());// 大家可以使用匿名内部类的方式(这个类没有名字。直接new接口。)TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {@Overridepublic int compare(WuGui o1, WuGui o2) {return o1.age - o2.age;}});wuGuis.add(new WuGui(1000));wuGuis.add(new WuGui(800));wuGuis.add(new WuGui(810));for(WuGui wuGui : wuGuis){System.out.println(wuGui);}}
}// 乌龟
class WuGui{int age;public WuGui(int age){this.age = age;}@Overridepublic String toString() {return "小乌龟[" +"age=" + age +']';}
}// 单独在这里编写一个比较器
// 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)
/*
class WuGuiComparator implements Comparator<WuGui> {@Overridepublic int compare(WuGui o1, WuGui o2) {// 指定比较规则// 按照年龄排序return o1.age - o2.age;}
}*/

最终的结论
放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
第一种:放在集合中的元素实现java.lang.Comparable接口。
第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
Comparable和Comparator怎么选择呢?
当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

Comparator接口的设计符合OCP原则。

Collections工具类

java.util.Collection 集合接口
java.util.Collections 集合工具类,方便集合的操作。

import java.util.*;public class CollectionsTest {public static void main(String[] args) {// ArrayList集合不是线程安全的。List<String> list = new ArrayList<>();// 变成线程安全的Collections.synchronizedList(list);// 排序list.add("abf");list.add("abx");list.add("abc");list.add("abe");Collections.sort(list);for(String s : list){System.out.println(s);}List<WuGui2> wuGuis = new ArrayList<>();wuGuis.add(new WuGui2(1000));wuGuis.add(new WuGui2(8000));wuGuis.add(new WuGui2(500));// 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。Collections.sort(wuGuis);for(WuGui2 wg : wuGuis){System.out.println(wg);}// 对Set集合怎么排序呢?Set<String> set = new HashSet<>();set.add("king");set.add("kingsoft");set.add("king2");set.add("king1");// 将Set集合转换成List集合List<String> myList = new ArrayList<>(set);Collections.sort(myList);for(String s : myList) {System.out.println(s);}// 这种方式也可以排序。//Collections.sort(list集合, 比较器对象);}
}class WuGui2 implements Comparable<WuGui2>{int age;public WuGui2(int age){this.age = age;}@Overridepublic int compareTo(WuGui2 o) {return this.age - o.age;}@Overridepublic String toString() {return "WuGui2{" +"age=" + age +'}';}
}

IO流

什么是IO?
I : Input
O : Output
通过IO可以完成硬盘文件的读和写。

IO流的分类

有多种分类方式:
一种方式是按照流的方向进行分类:
以内存作为参照物,
往内存中去,叫做输入(Input)。或者叫做读(Read)。
从内存中出来,叫做输出(Output)。或者叫做写(Write)。

另一种方式是按照读取数据方式不同进行分类:
有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。
这种流是万能的,什么类型的文件都可以读取,包括:文本文件,图片,声音文件,视频文件等…
假设文件file1.txt,采用字节流的话是这样读的:
a中国bc张三fe
第一次读:一个字节,正好读到’a’
第二次读:一个字节,正好读到’中’字符的一半。
第三次读:一个字节,正好读到’中’字符的另外一半。

有的流是按照字符的方式读取数据,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取纯文本文件,连word文件都无法读取。
假设文件file1.txt,采用字符流的话是这样读的:
a中国bc张三fe
第一次读:'a’字符('a’字符在windows系统中占用1个字节。)
第二次读:'中’字符('中’字符在windows系统中占用2个字节。)

综上所述:流的分类
输入流、输出流
字节流、字符流

Java中的IO流都已经写好了,我们不需要关心,我们最主要还是掌握:在java中已经提供了哪些流,每个流的特点是什么,每个流对象上的常用方法有哪些。
java中所有的流都是在:java.io.*;下
java中主要还是研究:
怎么new流对象。
调用流对象的哪个方法是读,哪个方法是写。

流的四大家族

四大家族的首领:
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流

java.io.Reader 字符输入流
java.io.Writer 字符输出流

四大家族的首领都是抽象类。(abstract class)

所有的流都实现了:
java.io.Closeable接口,都是可关闭的,都有**close()**方法。
流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。

所有的输出流都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法。
养成一个好习惯,输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!),刷新的作用就是清空管道。
注意:如果没有**flush()**可能会导致丢失数据。

注意:在java中只要“类名”以Stream结尾的都是字节流。以“Read/Writer”结尾的都是字符流。

java.io包下需要掌握的流有16个

文件专属:
java.io.FileInputStream(掌握)
java.io.FileOutputStream(掌握)
java.io.FileReader
java.io.FileWriter

转换流:(将字节流转换成字符流)
java.io.InputStreamReader
java.io.OutputStreamWriter

缓冲流专属:
java.io.BufferedReader
java.io.BufferedWriter
java.io.BufferedInputStream
java.io.BufferedOutputStream

数据流专属:
java.io.DataInputStream
java.io.DataOutputStream

标准输出流:
java.io.PrintWriter
java.io.PrintStream(掌握)

对象流专属:
java.io.ObjectInputStream(掌握)
java.io.ObjectOutputStream(掌握)

FileInputStream

java.io.FileInputStream:
1、文件字节输入流,万能的,任何类型的文件都可以采用这个流来读。
2、字节的方式,完成输入的操作,完成读的操作(硬盘—>内存)

初步使用

文件路径:F:\IdeaProjects\javase\temp.txt (IDEA会自动把\编程\,因为java中\表示转义)
F:/IdeaProjects/javase/temp.txt 写成这个/也是可以的。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class FileInputStreamTest01 {public static void main(String[] args) {FileInputStream fis = null;try {// 创建文件字节输入流对象// 文件路径:D:\course\JavaProjects\02-JavaSE\temp (IDEA会自动把\编程\\,因为java中\表示转义)// 以下都是采用了:绝对路径的方式。//FileInputStream fis = new FileInputStream("D:\\course\\JavaProjects\\02-JavaSE\\temp");// 写成这个/也是可以的。fis = new FileInputStream("D:/course/JavaProjects/02-JavaSE/temp");// 开始读int readData = fis.read(); // 这个方法的返回值是:读取到的“字节”本身。System.out.println(readData); //97readData = fis.read();System.out.println(readData); //98readData = fis.read();System.out.println(readData); //99readData = fis.read();System.out.println(readData); //100readData = fis.read();System.out.println(readData); //101readData = fis.read();System.out.println(readData); //102// 已经读到文件的末尾了,再读的时候读取不到任何数据,返回-1.readData = fis.read();System.out.println(readData);readData = fis.read();System.out.println(readData);readData = fis.read();System.out.println(readData);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {// 在finally语句块当中确保流一定关闭。if (fis != null) { // 避免空指针异常!// 关闭流的前提是:流不是空。流是null的时候没必要关闭。try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

循环读

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class FileInputStreamTest02 {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("D:\\course\\JavaProjects\\02-JavaSE\\temp");/*while(true) {int readData = fis.read();if(readData == -1) {break;}System.out.println(readData);}*/// 改造while循环int readData = 0;while((readData = fis.read()) != -1){System.out.println(readData);}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

分析这个程序的缺点:
一次读取一个字节byte,这样内存和硬盘交互太频繁,基本上时间/资源都耗费在交互上面了。能不能一次读取多个字节呢?可以。

IDEA中的当前路径

相对路径:相对路径一定是从当前所在的位置作为起点开始找!
IDEA默认的当前路径是哪里?工程Project的根就是IDEA的默认当前路径。

//fis = new FileInputStream("tempfile");
//fis = new FileInputStream("chapter23/tempfile2");
//fis = new FileInputStream("chapter23/src/tempfile3");
fis = new FileInputStream("chapter23/src/com/bjpowernode/java/io/tempfile4");

往byte数组中读

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class FileInputStreamTest03 {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("chapter23/src/com/bjpowernode/java/io/tempfile4");// 开始读,采用byte数组,一次读取多个字节。最多读取“数组.length”个字节。byte[] bytes = new byte[4]; // 准备一个4个长度的byte数组,一次最多读取4个字节。// 这个方法的返回值是:读取到的字节数量。(不是字节本身)int readCount = fis.read(bytes);System.out.println(readCount); // 第一次读到了4个字节。// 将字节数组全部转换成字符串//System.out.println(new String(bytes)); // abcd// 不应该全部都转换,应该是读取了多少个字节,转换多少个。System.out.println(new String(bytes,0, readCount));readCount = fis.read(bytes); // 第二次只能读取到2个字节。System.out.println(readCount); // 2// 将字节数组全部转换成字符串//System.out.println(new String(bytes)); // efcd// 不应该全部都转换,应该是读取了多少个字节,转换多少个。System.out.println(new String(bytes,0, readCount));readCount = fis.read(bytes); // 1个字节都没有读取到返回-1System.out.println(readCount); // -1} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

最终版(掌握)

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class FileInputStreamTest04 {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("chapter23/src/tempfile3");// 准备一个byte数组byte[] bytes = new byte[4];/*while(true){int readCount = fis.read(bytes);if(readCount == -1){break;}// 把byte数组转换成字符串,读到多少个转换多少个。System.out.print(new String(bytes, 0, readCount));}*/int readCount = 0;while((readCount = fis.read(bytes)) != -1) {System.out.print(new String(bytes, 0, readCount));}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

其它常用方法

int available() :返回流当中剩余的没有读到的字节数量。
这个方法有什么用?
byte[] bytes = new byte[fis.available()]; // 这种方式不太适合太大的文件,因为byte[]数组不能太大。

long skip(long n) :跳过几个字节不读。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class FileInputStreamTest05 {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("tempfile");System.out.println("总字节数量:" + fis.available());// 读1个字节//int readByte = fis.read();// 还剩下可以读的字节数量是:5//System.out.println("剩下多少个字节没有读:" + fis.available());// 这个方法有什么用?//byte[] bytes = new byte[fis.available()]; // 这种方式不太适合太大的文件,因为byte[]数组不能太大。// 不需要循环了。// 直接读一次就行了。//int readCount = fis.read(bytes); // 6//System.out.println(new String(bytes)); // abcdef// skip跳过几个字节不读取,这个方法也可能以后会用!fis.skip(3);System.out.println(fis.read()); //100} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

FileOutputStream

文件字节输出流,负责写。从内存到硬盘。
要想以追加的方式在文件末尾写入,构造FileOutputStream流对象时要带入true参数。

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;public class FileOutputStreamTest01 {public static void main(String[] args) {FileOutputStream fos = null;try {// myfile文件不存在的时候会自动新建!// 这种方式谨慎使用,这种方式会先将原文件清空,然后重新写入。//fos = new FileOutputStream("myfile");//fos = new FileOutputStream("chapter23/src/tempfile3");// 以追加的方式在文件末尾写入。不会清空原文件内容。fos = new FileOutputStream("chapter23/src/tempfile3", true);// 开始写。byte[] bytes = {97, 98, 99, 100};// 将byte数组全部写出!fos.write(bytes); // abcd// 将byte数组的一部分写出!fos.write(bytes, 0, 2); // 再写出ab// 字符串String s = "我是一个中国人,我骄傲!!!";// 将字符串转换成byte数组。byte[] bs = s.getBytes();// 写fos.write(bs);// 写完之后,最后一定要刷新fos.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}}
}

文件复制

使用FileInputStream + FileOutputStream完成文件的拷贝。
拷贝的过程应该是一边读,一边写
使用以上的字节流拷贝文件的时候,文件类型随意,万能的,什么样的文件都能拷贝。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;public class Copy01 {public static void main(String[] args) {FileInputStream fis = null;FileOutputStream fos = null;try {// 创建一个输入流对象fis = new FileInputStream("D:\\course\\02-JavaSE\\video\\chapter01\\动力节点-JavaSE-杜聚宾-001-文件扩展名的显示.avi");// 创建一个输出流对象fos = new FileOutputStream("C:\\动力节点-JavaSE-杜聚宾-001-文件扩展名的显示.avi");// 最核心的:一边读,一边写byte[] bytes = new byte[1024 * 1024]; // 1MB(一次最多拷贝1MB。)int readCount = 0;while((readCount = fis.read(bytes)) != -1) {fos.write(bytes, 0, readCount);}// 刷新,输出流最后要刷新fos.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {// 分开try,不要一起try。// 一起try的时候,其中一个出现异常,可能会影响到另一个流的关闭。if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}
}

FileReader

文件字符输入流,只能读取普通文本。读取文本内容时,比较方便、快捷。

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;public class FileReaderTest {public static void main(String[] args) {FileReader reader = null;try {// 创建文件字符输入流reader = new FileReader("tempfile");//准备一个char数组char[] chars = new char[4];// 往char数组中读reader.read(chars); // 按照字符的方式读取:第一次e,第二次f,第三次 风....for(char c : chars) {System.out.println(c);}/*// 开始读char[] chars = new char[4]; // 一次读取4个字符int readCount = 0;while((readCount = reader.read(chars)) != -1) {System.out.print(new String(chars,0,readCount));}*/} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (reader != null) {try {reader.close();} catch (IOException e) {e.printStackTrace();}}}}
}

FileWriter

文件字符输出流,写。只能输出普通文本。

import java.io.FileWriter;
import java.io.IOException;public class FileWriterTest {public static void main(String[] args) {FileWriter out = null;try {// 创建文件字符输出流对象//out = new FileWriter("file");out = new FileWriter("file", true);// 开始写。char[] chars = {'我','是','中','国','人'};out.write(chars);out.write(chars, 2, 3);out.write("我是一名java软件工程师!");// 写出一个换行符。out.write("\n");out.write("hello world!");// 刷新out.flush();} catch (IOException e) {e.printStackTrace();} finally {if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}}}
}

复制普通文本文件

使用FileReader + FileWriter进行拷贝的话,只能拷贝“普通文本”文件。(能使用记事本打开的都是普通文本)

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;public class Copy02 {public static void main(String[] args) {FileReader in = null;FileWriter out = null;try {// 读in = new FileReader("chapter23/src/com/bjpowernode/java/io/Copy02.java");// 写out = new FileWriter("Copy02.java");// 一边读一边写:char[] chars = new char[1024 * 512]; // 1MBint readCount = 0;while((readCount = in.read(chars)) != -1){out.write(chars, 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();}}}}
}

BufferedReader

带有缓冲区的字符输入流。
使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组。自带缓冲。

当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流
外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。
比如以下这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。

br.readLine()方法读取一个文本行,但不带换行符!!
对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭。(可以看源代码)

import java.io.BufferedReader;
import java.io.FileReader;public class BufferedReaderTest01 {public static void main(String[] args) throws Exception{FileReader reader = new FileReader("Copy02.java");// 当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。// 外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。// 像当前这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。BufferedReader br = new BufferedReader(reader);// 读一行/*String firstLine = br.readLine();System.out.println(firstLine);String secondLine = br.readLine();System.out.println(secondLine);String line3 = br.readLine();System.out.println(line3);*/// br.readLine()方法读取一个文本行,但不带换行符。String s = null;while((s = br.readLine()) != null){System.out.print(s);}// 关闭流// 对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭。(可以看源代码。)br.close();}
}

转换流InputStreamReader

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;public class BufferedReaderTest02 {public static void main(String[] args) throws Exception{/*// 字节流FileInputStream in = new FileInputStream("Copy02.java");// 通过转换流转换(InputStreamReader将字节流转换成字符流。)// in是节点流。reader是包装流。InputStreamReader reader = new InputStreamReader(in);// 这个构造方法只能传一个字符流。不能传字节流。// reader是节点流。br是包装流。BufferedReader br = new BufferedReader(reader);*/// 合并BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Copy02.java")));String line = null;while((line = br.readLine()) != null){System.out.println(line);}// 关闭最外层br.close();}
}

BufferedWriter

带有缓冲的字符输出流。
OutputStreamWriter:转换流

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;public class BufferedWriterTest {public static void main(String[] args) throws Exception{// 带有缓冲区的字符输出流//BufferedWriter out = new BufferedWriter(new FileWriter("copy"));BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy", true)));// 开始写。out.write("hello world!");out.write("\n");out.write("hello kitty!");// 刷新out.flush();// 关闭最外层out.close();}
}

数据流

java.io.DataOutputStream :数据字节输出流
这个流可以将数据连同数据的类型一并写入文件。
注意:这个文件不是普通文本文档。(这个文件使用记事本打不开)
java.io.DataInputStream :数据字节输入流
DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候需要提前知道写入的顺序。读的顺序需要和写的顺序一致,才可以正常取出数据。

import java.io.DataOutputStream;
import java.io.FileOutputStream;public class DataOutputStreamTest {public static void main(String[] args) throws Exception{// 创建数据专属的字节输出流DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));// 写数据byte b = 100;short s = 200;int i = 300;long l = 400L;float f = 3.0F;double d = 3.14;boolean sex = false;char c = 'a';// 写dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。dos.writeShort(s);dos.writeInt(i);dos.writeLong(l);dos.writeFloat(f);dos.writeDouble(d);dos.writeBoolean(sex);dos.writeChar(c);// 刷新dos.flush();// 关闭最外层dos.close();}
}
import java.io.DataInputStream;
import java.io.FileInputStream;public class DataInputStreamTest01 {public static void main(String[] args) throws Exception{DataInputStream dis = new DataInputStream(new FileInputStream("data"));// 开始读byte b = dis.readByte();short s = dis.readShort();int i = dis.readInt();long l = dis.readLong();float f = dis.readFloat();double d = dis.readDouble();boolean sex = dis.readBoolean();char c = dis.readChar();System.out.println(b);System.out.println(s);System.out.println(i + 1000);System.out.println(l);System.out.println(f);System.out.println(d);System.out.println(sex);System.out.println(c);dis.close();}
}

标准输出流

java.io.PrintStream :标准的字节输出流。默认输出到控制台。
标准输出流不需要手动close()关闭。
可以改变标准输出流的输出方向吗?可以。

import java.io.FileOutputStream;
import java.io.PrintStream;public class PrintStreamTest {public static void main(String[] args) throws Exception{// 联合起来写System.out.println("hello world!");// 分开写PrintStream ps = System.out;ps.println("hello zhangsan");ps.println("hello lisi");ps.println("hello wangwu");// 标准输出流不需要手动close()关闭。// 可以改变标准输出流的输出方向吗? 可以/*// 这些是之前System类使用过的方法和属性。System.gc();System.currentTimeMillis();PrintStream ps2 = System.out;System.exit(0);System.arraycopy(....);*/// 标准输出流不再指向控制台,指向“log”文件。PrintStream printStream = new PrintStream(new FileOutputStream("log"));// 修改输出方向,将输出方向修改到"log"文件。System.setOut(printStream);// 再输出System.out.println("hello world");System.out.println("hello kitty");System.out.println("hello zhangsan");}
}

日志工具

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;public class Logger {/*记录日志的方法。*/public static void log(String msg) {try {// 指向一个日志文件PrintStream out = new PrintStream(new FileOutputStream("log.txt", true));// 改变输出方向System.setOut(out);// 日期当前时间Date nowTime = new Date();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String strTime = sdf.format(nowTime);System.out.println(strTime + ": " + msg);} catch (FileNotFoundException e) {e.printStackTrace();}}
}
public class LogTest {public static void main(String[] args) {//测试工具类是否好用Logger.log("调用了System类的gc()方法,建议启动垃圾回收");Logger.log("调用了UserService的doSome()方法");Logger.log("用户尝试进行登录,验证失败");Logger.log("我非常喜欢这个记录日志的工具哦!");}
}

File类

1、File类和四大家族没有关系,所以File类不能完成文件的读和写。
2、File对象代表什么?
文件和目录路径名的抽象表示。
C:\Drivers 这是一个File对象
C:\Drivers\Lan\Realtek\Readme.txt 也是File对象。
一个File对象有可能对应的是目录,也可能是文件。
File只是一个路径名的抽象表示形式。
3、需要掌握File类中常用的方法。

常用方法

import java.io.File;public class FileTest01 {public static void main(String[] args) throws Exception {// 创建一个File对象File f1 = new File("D:\\file");// 判断是否存在!System.out.println(f1.exists());// 如果D:\file不存在,则以文件的形式创建出来/*if(!f1.exists()) {// 以文件形式新建f1.createNewFile();}*/// 如果D:\file不存在,则以目录的形式创建出来/*if(!f1.exists()) {// 以目录的形式新建。f1.mkdir();}*/// 可以创建多重目录吗?File f2 = new File("D:/a/b/c/d/e/f");/*if(!f2.exists()) {// 多重目录的形式新建。f2.mkdirs();}*/File f3 = new File("D:\\course\\01-开课\\学习方法.txt");// 获取文件的父路径String parentPath = f3.getParent();System.out.println(parentPath); //D:\course\01-开课File parentFile = f3.getParentFile();System.out.println("获取绝对路径:" + parentFile.getAbsolutePath());File f4 = new File("copy");System.out.println("绝对路径:" + f4.getAbsolutePath()); // C:\Users\Administrator\IdeaProjects\javase\copy}
}
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;public class FileTest02 {public static void main(String[] args) {File f1 = new File("D:\\course\\01-开课\\开学典礼.ppt");// 获取文件名System.out.println("文件名:" + f1.getName());// 判断是否是一个目录System.out.println(f1.isDirectory()); // false// 判断是否是一个文件System.out.println(f1.isFile()); // true// 获取文件最后一次修改时间long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。// 将总毫秒数转换成日期?????Date time = new Date(haoMiao);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");String strTime = sdf.format(time);System.out.println(strTime);// 获取文件大小System.out.println(f1.length()); //216064字节。}
}
import java.io.File;/*
File中的listFiles方法。*/
public class FileTest03 {public static void main(String[] args) {// File[] listFiles()// 获取当前目录下所有的子文件。File f = new File("D:\\course\\01-开课");File[] files = f.listFiles();// foreachfor(File file : files){//System.out.println(file.getAbsolutePath());System.out.println(file.getName());}}
}

目录拷贝

import java.io.*;/*
拷贝目录*/
public class CopyAll {public static void main(String[] args) {// 拷贝源File srcFile = new File("D:\\course\\02-JavaSE\\document");// 拷贝目标File destFile = new File("C:\\a\\b\\c");// 调用方法拷贝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 {// 读这个文件// D:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdfin = new FileInputStream(srcFile);// 写到这个文件中// C:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdfString path = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\")  + srcFile.getAbsolutePath().substring(3);out = new FileOutputStream(path);// 一边读一边写byte[] bytes = new byte[1024 * 1024]; // 一次复制1MBint 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 (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}}return;}// 获取源下面的子目录File[] files = srcFile.listFiles();for(File file : files){// 获取所有文件的(包括目录和文件)绝对路径//System.out.println(file.getAbsolutePath());if(file.isDirectory()){// 新建对应的目录//System.out.println(file.getAbsolutePath());//D:\course\02-JavaSE\document\JavaSE进阶讲义       源目录//C:\course\02-JavaSE\document\JavaSE进阶讲义       目标目录String srcDir = file.getAbsolutePath();String destDir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\")  + srcDir.substring(3);File newFile = new File(destDir);if(!newFile.exists()){newFile.mkdirs();}}// 递归调用copyDir(file, destFile);}}
}
package com.bjpowernode.javase.io;import java.io.*;public class CopyAll {public static void main(String[] args) {File srcFile = new File("E:\\安装包\\百度网盘\\老杜2020版Java零基础685集(视频采用JDK13录制)\\002-JavaSE每章作业");File destFile = new File("F:\\");copyDir(srcFile,destFile);}/*** 拷贝目录* @param srcFile 拷贝源* @param destFile 拷贝目标*/private static void copyDir(File srcFile, File destFile) {//在目标路径下建当前目录File destDic = null;if (srcFile.isDirectory()) {String destDicName = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getName();destDic = new File(destDicName);if (!destDic.exists()) {destDic.mkdirs();}}//如果srcFile是一个文件的话,递归结束if (srcFile.isFile()){FileInputStream fis = null;FileOutputStream fos = null;try {fis = new FileInputStream(srcFile);String destFilePath = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getName();fos = new FileOutputStream(destFilePath);byte[] bytes = new byte[1024*1024];int readCount = -1;while ((readCount = fis.read(bytes)) != -1){fos.write(bytes, 0, readCount);}fos.flush();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}if (fis != null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}}return;}//获取源下面的子目录File[] files = srcFile.listFiles();for (File file : files){copyDir(file, destDic);}}
}

对象流

序列化和反序列化

1、参与序列化和反序列化的对象,必须实现Serializable接口。不然会报错:java.io.NotSerializableException(对象不支持序列化)。

2、注意:通过源代码发现,Serializable接口只是一个标志接口
public interface Serializable {
}
这个接口当中什么代码都没有。
那么它起到一个什么作用呢?
起到标识/标志的作用,java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇。
Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号

序列化对象

import com.bjpowernode.java.bean.Student;import java.io.FileOutputStream;
import java.io.ObjectOutputStream;public class ObjectOutputStreamTest01 {public static void main(String[] args) throws Exception{// 创建java对象Student s = new Student(1111, "zhangsan");// 序列化ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));// 序列化对象oos.writeObject(s);// 刷新oos.flush();// 关闭oos.close();}
}

反序列化对象

import java.io.FileInputStream;
import java.io.ObjectInputStream;public class ObjectInputStreamTest01 {public static void main(String[] args) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));// 开始反序列化,读Object obj = ois.readObject();// 反序列化回来是一个学生对象,所以会调用学生对象的toString方法。System.out.println(obj);ois.close();}
}

一次序列化多个对象

可以将对象放到集合当中,序列化集合。
提示:
参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable接口。

import com.bjpowernode.java.bean.User;import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;public class ObjectOutputStreamTest02 {public static void main(String[] args) throws Exception{List<User> userList = new ArrayList<>();userList.add(new User(1,"zhangsan"));userList.add(new User(2, "lisi"));userList.add(new User(3, "wangwu"));ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));// 序列化一个集合,这个集合对象中放了很多其他对象。oos.writeObject(userList);oos.flush();oos.close();}
}

反序列化集合

import com.bjpowernode.java.bean.User;import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;public class ObjectInputStreamTest02 {public static void main(String[] args) throws Exception{ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));//Object obj = ois.readObject();//System.out.println(obj instanceof List);List<User> userList = (List<User>)ois.readObject();for(User user : userList){System.out.println(user);}ois.close();}
}

transient关键字

表示游离的,不参与序列化。

该属性不会被序列化进文件中,反序列化后的值为类型默认值。

关于序列化版本号

序列化版本号有什么用呢?
java.io.InvalidClassException:
com.bjpowernode.java.bean.Student;
local class incompatible:
stream classdesc serialVersion = -684255398724514298(十年后)。
local class serialVersionUID = -3463447116624555755(十年前)。

过了十年Student这个类源代码改动了,源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件。并且class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改变。

java语言中是采用什么机制来区分类的?
第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类。
第二:如果类名一样,再怎么进行类的区别?靠序列化版本号进行区分

例:
小鹏编写了一个类:com.bjpowernode.java.bean.Student implements Srializable
小明编写了一个类:com.bjpowernode.java.bean.Student implements Srializable
不同的人编写了同一个类,但”这两个类确实不是同一个类“,这个时候序列化版本号就起作用了。
对于java虚拟机来说,java虚拟机是可以区分开这两个类的,因为这两个类都实现了Serializable接口,都有默认的序列化版本号,他们的序列化版本号不一样。所以区分开了(这是自动生成序列化版本号的好处)

请思考?
这种自动生成序列化版本号有什么缺陷?
这种自动生成的序列化版本号缺点是:一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类。

最终结论
凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号。
这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类。

建议将序列化版本号手动的写出来。不建议自动生成。
private static final long serialVersionUID = 1L; // java虚拟机识别一个类的时候先通过类名,如果类名一致,再通过序列化版本号。

IO和Properties的联合使用

非常好的一个设计理念:
以后经常改变的数据,可以单独写到一个文件中,使用程序动态读取。将来只需要修改这个文件的内容,java代码不需要改动,不需要重新编译,服务器也不需要重启。就可以拿到动态的信息。

类似于以上机制的这种文件被称为配置文件
并且当配置文件中的内容格式是:
key1=value
key2=value
的时候,我们把这种配置文件叫做属性配置文件
(建议key和value之间使用=的方式,属性配置文件的key重复的话,value会自动覆盖!)
(最好不要有空格)

java规范中有要求:属性配置文件建议以.properties结尾,但这不是必须的。
这种以.properties结尾的文件在java中被称为:属性配置文件。
其中java.util.Properties是专门存放属性配置文件内容的一个类。

import java.io.FileReader;
import java.util.Properties;public class IoPropertiesTest01 {public static void main(String[] args) throws Exception{/*Properties是一个Map集合,key和value都是String类型。想将userinfo文件中的数据加载到Properties对象当中。*/// 新建一个输入流对象FileReader reader = new FileReader("chapter23/userinfo.properties");// 新建一个Map集合Properties pro = new Properties();// 调用Properties对象的load方法将文件中的数据加载到Map集合中。pro.load(reader); // 文件中的数据顺着管道加载到Map集合中,其中等号=左边做key,右边做value// 通过key来获取value呢?String username = pro.getProperty("username");System.out.println(username);String password = pro.getProperty("password");System.out.println(password);String data = pro.getProperty("data");System.out.println(data);String usernamex = pro.getProperty("usernamex");System.out.println(usernamex);}
}

多线程

1、什么是进程?什么是线程?
进程是一个运行的应用程序(1个进程是一个软件,进程是动态的)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。

2、对于java程序来说,当在DOS命令窗口中输入:java HelloWorld 回车之后
会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法,同时启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

3、进程和线程是什么关系?举个例子
阿里巴巴:进程
马云:阿里巴巴的一个线程
童文红:阿里巴巴的一个线程

京东:进程
强东:京东的一个线程
妹妹:京东的一个线程

进程可以看做是现实生活中的公司。
线程可以看做是公司当中的某个员工。

注意:
进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)
魔兽游戏是一个进程
酷狗音乐是一个进程
这两个进程是独立的,不共享资源。

线程A和线程B呢?
在java语言中:
线程A和线程B,堆内存和方法区内存共享
但是栈内存独立,一个线程一个栈。

假设启动10个线程,会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发

火车站,可以看作是一个进程。
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
所以多线程并发可以提高效率。

java中之所以有多线程机制,目的就是为了提高程序的处理效率。

一个线程一个栈

4、思考一个问题:
使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束?
main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。

多线程并发的理解

5、分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?不能。
对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个线程并发执行(并行)。

什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。

单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种**“多线程并发”的感觉**。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时在做!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,给我们的感觉是同时并发的。


分析以下程序,除垃圾回收线程之外,有几个线程?
1个线程。(因为程序只有1个栈。)没有启动分支栈,没有启动分支线程。只有一个主线程。

public class ThreadTest01 {public static void main(String[] args) {System.out.println("main begin");m1();System.out.println("main over");}private static void m1() {System.out.println("m1 begin");m2();System.out.println("m1 over");}private static void m2() {System.out.println("m2 begin");m3();System.out.println("m2 over");}private static void m3() {System.out.println("m3 execute!");}
}

实现线程的第一种方式

编写一个类,直接继承java.lang.Thread,重写run方法。

怎么创建线程对象? new就行了。
怎么启动线程呢? 调用线程对象的**start()**方法。

注意:
亘古不变的道理:
方法体当中的代码永远都是自上而下的顺序依次逐行执行的。

public class ThreadTest02 {public static void main(String[] args) {// 这里是main方法,这里的代码属于主线程,在主栈中运行。// 新建一个分支线程对象MyThread t = new MyThread();// 启动线程//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)// start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。// 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。// 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。// run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。t.start();// 这里的代码还是运行在主线程中。for(int i = 0; i < 1000; i++){System.out.println("主线程--->" + i);}}
}class MyThread extends Thread {@Overridepublic void run() {// 编写程序,这段程序运行在分支线程中(分支栈)。for(int i = 0; i < 1000; i++){System.out.println("分支线程--->" + i);}}
}

直接调用run()方法

不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)

调用start()方法

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间(分支栈),只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

实现线程的第二种方式

编写一个类,实现java.lang.Runnable接口,重写run()方法。
创建该类的对象,将可运行的对象封装成一个线程对象,然后启动线程。(该类并不是一个线程类,只是一个可运行的类,它还不是一个线程。)

public class ThreadTest03 {public static void main(String[] args) {// 创建一个可运行的对象//MyRunnable r = new MyRunnable();// 将可运行的对象封装成一个线程对象//Thread t = new Thread(r);Thread t = new Thread(new MyRunnable()); // 合并代码// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("主线程--->" + i);}}
}// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("分支线程--->" + i);}}
}

采用匿名内部类的写法

public class ThreadTest04 {public static void main(String[] args) {// 创建线程对象,采用匿名内部类方式。// 这是通过一个没有名字的类,new出来的对象。Thread t = new Thread(new Runnable(){@Overridepublic void run() {for(int i = 0; i < 100; i++){System.out.println("t线程---> " + i);}}});// 启动线程t.start();for(int i = 0; i < 100; i++){System.out.println("main线程---> " + i);}}
}

线程生命周期

获取当前线程对象

static Thread currentThread() :返回对当前正在执行的线程对象的引用。
例:Thread t = Thread.currentThread();返回值t就是当前线程。

获取线程对象的名字

String name = 线程对象.getName();

修改线程对象的名字
线程对象.setName(“线程名字”);

当线程没有设置名字的时候,默认的名字有什么规律?
Thread-0
Thread-1
Thread-2

public class ThreadTest05 {public void doSome(){// 这样就不行了//this.getName();//super.getName();// 但是这样可以String name = Thread.currentThread().getName();System.out.println("------->" + name);}public static void main(String[] args) {ThreadTest05 tt = new ThreadTest05();tt.doSome();//currentThread就是当前线程对象// 这个代码出现在main方法当中,所以当前线程就是主线程。Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName()); //main// 创建线程对象MyThread2 t = new MyThread2();// 设置线程的名字t.setName("t1");// 获取线程的名字String tName = t.getName();System.out.println(tName); //Thread-0MyThread2 t2 = new MyThread2();t2.setName("t2");System.out.println(t2.getName()); //Thread-1\t2.start();// 启动线程t.start();}
}class MyThread2 extends Thread {public void run(){for(int i = 0; i < 100; i++){// currentThread就是当前线程对象。当前线程是谁呢?// 当t1线程执行run方法,那么这个当前线程就是t1// 当t2线程执行run方法,那么这个当前线程就是t2Thread currentThread = Thread.currentThread();System.out.println(currentThread.getName() + "-->" + i);//System.out.println(super.getName() + "-->" + i);//System.out.println(this.getName() + "-->" + i);}}
}

线程的sleep()方法

static void sleep(long millis)
1、静态方法:Thread.sleep(1000);
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
4、Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。

public class ThreadTest06 {public static void main(String[] args) {// 让当前线程进入休眠,睡眠5秒// 当前线程是主线程!!!/*try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}*/// 5秒之后执行这里的代码//System.out.println("hello world!");for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);// 睡眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

关于Thread.sleep()的面试题

public class ThreadTest07 {public static void main(String[] args) {// 创建线程对象Thread t = new MyThread3();t.setName("t");t.start();// 调用sleep方法try {// 问题:这行代码会让线程t进入休眠状态吗?t.sleep(1000 * 5); // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);// 这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。// 这样代码出现在main方法中,main线程睡眠。} catch (InterruptedException e) {e.printStackTrace();}// 5秒之后这里才会执行。System.out.println("hello World!");}
}class MyThread3 extends Thread {public void run(){for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}

终止线程的睡眠

sleep睡眠太久了,如果希望半道上醒来,应该怎么办?也就是说怎么叫醒一个正在睡眠的线程?
注意:这个不是中断线程的执行,是终止线程的睡眠

重点:run()当中的异常不能throws,只能try catch。
因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。

中断t线程的睡眠(这种中断睡眠的方式依靠了java的异常处理机制。)
t.interrupt; //干扰,一盆冷水过去!

public class ThreadTest08 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable2());t.setName("t");t.start();// 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)t.interrupt(); // 干扰,一盆冷水过去!}
}class MyRunnable2 implements Runnable {// 重点:run()当中的异常不能throws,只能try catch// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "---> begin");try {// 睡眠1年Thread.sleep(1000 * 60 * 60 * 24 * 365);} catch (InterruptedException e) {// 打印异常信息//e.printStackTrace();}//1年之后才会执行这里System.out.println(Thread.currentThread().getName() + "---> end");// 调用doOther//doOther();}// 其它方法可以throws/*public void doOther() throws Exception{}*/
}

强行终止线程的执行

这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。

public class ThreadTest09 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable3());t.setName("t");t.start();// 模拟5秒try {Thread.sleep(1000 * 5);} catch (InterruptedException e) {e.printStackTrace();}// 5秒之后强行终止t线程t.stop(); // 已过时(不建议使用。)}
}class MyRunnable3 implements Runnable {@Overridepublic void run() {for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

合理的终止线程的执行

public class ThreadTest10 {public static void main(String[] args) {MyRunable4 r = new MyRunable4();Thread t = new Thread(r);t.setName("t");t.start();// 模拟5秒try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}// 终止线程// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。r.run = false;}
}class MyRunable4 implements Runnable {// 打一个布尔标记boolean run = true;@Overridepublic void run() {for (int i = 0; i < 10; i++){if(run){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}else{// return就结束了,你在结束之前还有什么没保存的。// 在这里可以保存呀。//save....//终止当前线程return;}}

线程调度

常见的线程调度模型

抢占式调度模型:
哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。(处于运行状态的时间多一些)
java采用的就是抢占式调度模型。

均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
有一些编程语言,线程调度模型采用的就是这种方式。

线程调度的方法

java中提供了哪些方法是和线程调度有关系的呢?

实例方法:
**void setPriority(int newPriority) **:设置线程的优先级
int getPriority() :获取线程优先级
最低优先级1 MIN_PRIORITY
默认优先级5 NORM_PRIORITY
最高优先级10 MAX_PRIORITY
优先级比较高的获取CPU时间片可能会多一些。

静态方法:
static void yield() :让位方法
暂停当前正在执行的线程对象,并执行其它线程。
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。

实例方法:
void join() :等待这个线程死亡
合并线程。
class MyThread1 extends Thread{
public void doSome(){
MyThread2 t = new MyThread2();
t.join; //当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
}
}
class MyThread2 extends Thread{
}

线程优先级

public class ThreadTest11 {public static void main(String[] args) {// 设置主线程的优先级为1Thread.currentThread().setPriority(1);/*System.out.println("最高优先级" + Thread.MAX_PRIORITY);System.out.println("最低优先级" + Thread.MIN_PRIORITY);System.out.println("默认优先级" + Thread.NORM_PRIORITY);*/// 获取当前线程对象,获取当前线程的优先级Thread currentThread = Thread.currentThread();// main线程的默认优先级是:5//System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());Thread t = new Thread(new MyRunnable5());t.setPriority(10);t.setName("t");t.start();// 优先级较高的,只是抢到的CPU时间片相对多一些。// 大概率方向更偏向于优先级比较高的。for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "-->" + i);}}
}class MyRunnable5 implements Runnable {@Overridepublic void run() {// 获取线程优先级//System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "-->" + i);}}
}

线程让位

public class ThreadTest12 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable6());t.setName("t");t.start();for(int i = 1; i <= 10000; i++) {System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}class MyRunnable6 implements Runnable {@Overridepublic void run() {for(int i = 1; i <= 10000; i++) {//每100个让位一次。if(i % 100 == 0){Thread.yield(); // 当前线程暂停一下,让给主线程。}System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}

线程合并

public class ThreadTest13 {public static void main(String[] args) {System.out.println("main begin");Thread t = new Thread(new MyRunnable7());t.setName("t");t.start();//合并线程try {t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main over");}
}class MyRunnable7 implements Runnable {@Overridepublic void run() {for(int i = 0; i < 10000; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);}}
}

线程安全

关于多线程并发环境下,数据的安全问题。
1、为什么这个是重点
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等,都已经实现完了。这些代码我们都不需要编写。

最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。

2、什么时候数据在多线程并发的环境下会存在安全问题呢?(线程不安全的条件)
三个条件
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题

怎么解决线程安全问题

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)。用排队执行解决线程安全问题。这种机制被称为:线程同步机制
专业术语:线程同步,实际上就是线程不能并发了,线程必须排队执行

面试:
怎么解决线程安全问题呀? 使用“线程同步机制”。

线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的什么事了。

两种编程模型

异步编程模型
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高)。

异步就是并发。

同步编程模型
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行

同步就是排队。

模拟两个线程对同一个账户取款

不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。

public class Account {// 账号private String actno;// 余额private double balance;public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法public void withdraw(double money){// t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)// 取款之前的余额double before = this.getBalance(); // 10000// 取款之后的余额double after = before - money;// 在这里模拟一下网络延迟,100%会出现问题try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 更新余额// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。this.setBalance(after);}
}
public class AccountThread extends Thread {// 两个线程必须共享同一个账户对象。private Account act;// 通过构造方法传递过来账户对象public AccountThread(Account act) {this.act = act;}public void run(){// run方法的执行表示取款操作。// 假设取款5000double money = 5000;// 取款// 多线程并发执行这个方法。act.withdraw(money);System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());}
}
public class Test {public static void main(String[] args) {// 创建账户对象(只创建1个)Account act = new Account("act-001", 10000);// 创建两个线程Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);// 设置namet1.setName("t1");t2.setName("t2");// 启动线程取款t1.start();t2.start();}
}

同步代码块synchronized

线程同步机制的语法是:
synchronized(){
//线程同步代码块。
}
synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据。才能达到多线程排队。

()中写什么?
那要看你想让哪些线程同步。
假设t1、t2、t3、t4、t5,有5个线程。
你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。

在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁)
100个对象,100把锁。1个对象1把锁。

以下代码的执行原理?
1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。

这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。

/*
银行账户使用线程同步机制,解决线程安全问题。*/
public class Account {// 账号private String actno;// 余额private double balance; //实例变量。//对象Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)public Account() {}public Account(String actno, double balance) {this.actno = actno;this.balance = balance;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}//取款的方法public void withdraw(double money){// 以下这几行代码必须是线程排队的,不能并发。// 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。/*这里的共享对象是:账户对象。账户对象是共享的,那么this就是账户对象吧!!!不一定是this,这里只要是多线程共享的那个对象就行。*///Object obj2 = new Object();//synchronized (this){//synchronized (obj) {//synchronized ("abc") { // "abc"在字符串常量池当中。//synchronized (null) { // 报错:空指针。//synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。double before = this.getBalance();double after = before - money;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);//}}
}

synchronized(“abc”) 相当于你取款时要让全天下人都等。
不应该,只有同时对你这一个账户进行取款的时候,需要等待,别人取钱的时候,是从其他账户中取钱,不需要等待。

哪些变量有线程安全问题

Java中有三大变量
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。

以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈)
局部变量在栈中。所以局部变量永远都不会共享。

实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。

局部变量和常量:不会有线程安全问题。(常量不可变)
成员变量:可能会有线程安全问题。

如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。StringBuffer效率比较低。

ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。

synchronized出现在实例方法上

在实例方法上可以使用synchronized吗?可以的。
synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他的对象了。所以这种方式不灵活。

另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。

synchronized使用在实例方法上有什么优点?
代码写的少了,节俭了。

如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。

synchronized的三种写法

第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。

对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。

面试题

面试题1:doOther方法执行的时候需要等待doSome方法的结束吗?
不需要,因为doOther()方法没有synchronized

public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc = new MyClass();Thread t1 = new MyThread(mc);Thread t2 = new MyThread(mc);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}
}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}
}class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}

面试题2:doOther方法执行的时候需要等待doSome方法的结束吗?
需要。

class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}

面试题3:doOther方法执行的时候需要等待doSome方法的结束吗?
不需要,因为MyClass对象是两个,两把锁。

public class Exam01 {public static void main(String[] args) throws InterruptedException {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new MyThread(mc1);Thread t2 = new MyThread(mc2);t1.setName("t1");t2.setName("t2");t1.start();Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。t2.start();}
}class MyThread extends Thread {private MyClass mc;public MyThread(MyClass mc){this.mc = mc;}public void run(){if(Thread.currentThread().getName().equals("t1")){mc.doSome();}if(Thread.currentThread().getName().equals("t2")){mc.doOther();}}
}class MyClass {public synchronized void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}

面试题4:doOther方法执行的时候需要等待doSome方法的结束吗?
需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。

class MyClass {// synchronized出现在静态方法上是找类锁。public synchronized static void doSome(){System.out.println("doSome begin");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public synchronized static void doOther(){System.out.println("doOther begin");System.out.println("doOther over");}
}

死锁

一般面试官要求会写,只有会写的,才会在以后的开发中注意这个事。因为死锁很难调试。

public class DeadLock {public static void main(String[] args) {Object o1 = new Object();Object o2 = new Object();// t1和t2两个线程共享o1,o2Thread t1 = new MyThread1(o1,o2);Thread t2 = new MyThread2(o1,o2);t1.start();t2.start();}
}class MyThread1 extends Thread{Object o1;Object o2;public MyThread1(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o1){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){}}}
}class MyThread2 extends Thread {Object o1;Object o2;public MyThread2(Object o1,Object o2){this.o1 = o1;this.o2 = o2;}public void run(){synchronized (o2){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){}}}
}

开发中怎么解决线程安全问题

是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量(并发量)降低。用户体验差。在不得已的情况下再选择线程同步机制。

第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

守护线程

java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程。)

守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

注意:主线程main方法是一个用户线程。

守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,每到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。

public class ThreadTest14 {public static void main(String[] args) {Thread t = new BakDataThread();t.setName("备份数据的线程");// 启动线程之前,将线程设置为守护线程t.setDaemon(true);t.start();// 主线程:主线程是用户线程for(int i = 0; i < 10; i++){System.out.println(Thread.currentThread().getName() + "--->" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}class BakDataThread extends Thread {public void run(){int i = 0;// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。while(true){System.out.println(Thread.currentThread().getName() + "--->" + (++i));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

定时器

定时器的作用:
间隔特定的时间,执行特定的程序。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种方式实现:
1、可以使用sleep方法,设置睡眠时间,每到这个时间点醒来执行任务。这种方式是最原始的定时器。(比较low)
2、在java的类库中已经写好一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
3、在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

public class TimerTest {public static void main(String[] args) throws Exception {// 创建定时器对象Timer timer = new Timer();//Timer timer = new Timer(true); //守护线程的方式// 指定定时任务//timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date firstTime = sdf.parse("2020-03-14 09:34:30");//timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);// 每年执行一次。//timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);//匿名内部类方式timer.schedule(new TimerTask(){@Overridepublic void run() {// code....}} , firstTime, 1000 * 10);}
}// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {@Overridepublic void run() {// 编写你需要执行的任务就行了。SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String strTime = sdf.format(new Date());System.out.println(strTime + ":成功完成了一次数据备份!");}
}

实现线程的第三种方式

实现Callable接口。(JDK8新特性)
这种方式实现的线程可以获取线程的返回值。
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

涉及:
java.util.Callable;
java.util.concurrent.FutureTask; //JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask; // JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。public class ThreadTest15 {public static void main(String[] args) throws Exception {// 第一步:创建一个“未来任务类”对象。// 参数非常重要,需要给一个Callable接口实现类对象。FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值// 线程执行一个任务,执行之后可能会有一个执行结果// 模拟执行System.out.println("call method begin");Thread.sleep(1000 * 10);System.out.println("call method end!");int a = 100;int b = 200;return a + b; //自动装箱(300结果变成Integer)}});// 创建线程对象Thread t = new Thread(task);// 启动线程t.start();// 这里是main方法,这是在主线程中。// 在主线程中,怎么获取t线程的返回结果?// get()方法的执行会导致“当前线程阻塞”Object obj = task.get();System.out.println("线程执行结果:" + obj);// main方法这里的程序要想执行必须等待get()方法的结束// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果// 另一个线程执行是需要时间的。System.out.println("hello world!");}
}

wait和notify

关于Object类中的wait和notify方法。(生产者和消费者模式!)

第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是Object类中自带的
wait方法和notify方法不是通过线程对象调用的。

第二:**wait()**方法作用?
Object o = new Object();
o.wait();
表示:
让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。

第三:**notify()方法作用?
Object o = new Object();
o.notify();
表示:
唤醒正在o对象上等待的线程。
还有一个
notifyAll()**方法:
这个方法是唤醒o对象上处于等待的所有线程。

生产者和消费者模式

实现

1、使用wait方法和notify方法实现“生产者和消费者模式”。

2、什么是“生产者和消费者模式”?
生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。

3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。

4、wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题。

5、wait方法作用:o.wait()让正在o对象上活动的t线程进入等待状态,并且释放掉t线程占有的o对象的锁。

6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上占有的锁。

7、模拟这样一个需求:
仓库我们采用List集合。
List集合中假设只有存储1个元素。
1个元素就表示仓库满了。
如果List集合中元素个数是0,就表示仓库空了。
保证List集合中永远都是最多存储1个元素。
必须做到这种效果:生产1个消费1个。

import java.util.ArrayList;
import java.util.List;public class ThreadTest16 {public static void main(String[] args) {// 创建1个仓库对象,共享的。List list = new ArrayList();// 创建两个线程对象// 生产者线程Thread t1 = new Thread(new Producer(list));// 消费者线程Thread t2 = new Thread(new Consumer(list));t1.setName("生产者线程");t2.setName("消费者线程");t1.start();t2.start();}
}// 生产线程
class Producer implements Runnable {// 仓库private List list;public Producer(List list) {this.list = list;}@Overridepublic void run() {// 一直生产(使用死循环来模拟一直生产)while(true){// 给仓库对象list加锁。synchronized (list){if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。try {// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒消费者进行消费list.notifyAll();}}}
}// 消费线程
class Consumer implements Runnable {// 仓库private List list;public Consumer(List list) {this.list = list;}@Overridepublic void run() {// 一直消费while(true){synchronized (list) {if(list.size() == 0){try {// 仓库已经空了。// 消费者线程等待,释放掉list集合的锁list.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 程序能够执行到此处说明仓库中有数据,进行消费。Object obj = list.remove(0);System.out.println(Thread.currentThread().getName() + "--->" + obj);// 唤醒生产者生产。list.notifyAll();}}}
}

反射机制

1、反射机制有什么用?
通过java语言中的反射机制可以操作字节码文件。有点类似于黑客。(可以读和修改字节码文件。)
通过反射机制可以操作代码片段。(class文件)

2、反射机制的相关类在哪个包下?
java.lang.reflect.*;

3、反射机制相关的重要的类有哪些?
java.lang.Class :代表整个字节码,代表一个类型。代表整个类。
java.lang.reflect.Method :代表字节码中的方法字节码。代表类中的方法。
java.lang.reflect.Constructor :代表字节码中的构造方法字节码。代表类中的构造方法。
java.lang.reflect.Field :代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。

获取Class的三种方式


要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例?
三种方式
第一种:Class c = Class.forName(“完整类名带包名”);
第二种:Class c = 对象.getClass();
第三种:Class c = 任何类型.class;

import java.util.Date;public class ReflectTest01 {public static void main(String[] args) {/*Class.forName()1、静态方法2、方法的参数是一个字符串。3、字符串需要的是一个完整类名。4、完整类名必须带有包名。java.lang包也不能省略。*/Class c1 = null;Class c2 = null;try {c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型。c2 = Class.forName("java.util.Date"); // c2代表Date类型Class c3 = Class.forName("java.lang.Integer"); // c3代表Integer类型Class c4 = Class.forName("java.lang.System"); // c4代表System类型} catch (ClassNotFoundException e) {e.printStackTrace();}// java中任何一个对象都有一个方法:getClass()String s = "abc";Class x = s.getClass(); // x代表String.class字节码文件,x代表String类型。System.out.println(c1 == x); // true(==判断的是对象的内存地址。)Date time = new Date();Class y = time.getClass();System.out.println(c2 == y); // true (c2和y两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件。)// 第三种方式,java语言中任何一种类型,包括基本数据类型,它都有.class属性。Class z = String.class; // z代表String类型Class k = Date.class; // k代表Date类型Class f = int.class; // f代表int类型Class e = double.class; // e代表double类型System.out.println(x == z); // true}
}

通过反射实例化对象

获取到Class,能干什么?
通过Class的newInstance()方法来实例化对象。
重点:newInstance()方法内部实际上调用了无参构造方法,必须保证无参构造方法是存在的!

import com.bjpowernode.java.bean.User;public class ReflectTest02 {public static void main(String[] args) {// 这是不使用反射机制,创建对象User user = new User();System.out.println(user);// 下面这段代码是以反射机制的方式创建对象。try {// 通过反射机制,获取Class,通过Class来实例化对象Class c = Class.forName("com.bjpowernode.java.bean.User"); // c代表User类型。// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!Object obj = c.newInstance();System.out.println(obj); // com.bjpowernode.java.bean.User@10f87f48} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}}
}

通过读属性文件实例化对象

验证反射机制的灵活性。
java代码写一遍,在不改变java源代码的基础之上,可以做到不同对象的实例化。
非常灵活。(符合OCP原则:对扩展开放,对修改关闭。)

import java.io.FileReader;
import java.util.Properties;public class ReflectTest03 {public static void main(String[] args) throws Exception{// 这种方式代码就写死了。只能创建一个User类型的对象//User user = new User();// 以下代码是灵活的,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建出不同的实例对象。// 通过IO流读取classinfo.properties文件FileReader reader = new FileReader("chapter25/classinfo2.properties");// 创建属性类对象MapProperties pro = new Properties(); // key value都是String// 加载pro.load(reader);// 关闭流reader.close();// 通过key获取valueString className = pro.getProperty("className");//System.out.println(className);// 通过反射机制实例化对象Class c = Class.forName(className);Object obj = c.newInstance();System.out.println(obj);}
}

只让静态代码块执行

只让静态代码块执行可以使用forName。
研究一下:Class.forName()发生了什么?
记住,重点:
如果只是希望一个类的静态代码块执行,其它代码一律不执行,你可以使用:
Class.forName(“完整类名”);
这个方法的执行会导致类加载,类加载时,静态代码块执行。

提示:后面JDBC技术的时候需要用到。

public class ReflectTest04 {public static void main(String[] args) {try {// Class.forName()这个方法的执行会导致:类加载。Class.forName("com.bjpowernode.java.reflect.MyClass");} catch (ClassNotFoundException e) {e.printStackTrace();}}
}

获取类路径下文件的绝对路径

研究一下文件路径的问题。
怎么获取一个文件的绝对路径?以下讲解的这种方式是通用的。但前提是:文件需要在类路径下,才能用这种方式。

这种方式的路径缺点是:移植性差,在IDEA中默认的当前路径是project的根。
这个代码假设离开了IDEA,换到了其它位置,可能当前路径就不是project的根了,这时这个路径就无效了。
FileReader reader = new FileReader("chapter25/classinfo2.properties");

接下来说一种比较通用的一种路径。即使代码换位置了,这样编写仍然是通用的。
注意:使用以下通用方式的前提是:这个文件必须在类路径下
什么是类路径下?凡是在src下的都是类路径下。【记住它】
src是类的根路径

import java.io.FileReader;public class AboutPath {public static void main(String[] args) throws Exception{/*解释:Thread.currentThread() 当前线程对象getContextClassLoader() 是线程对象的方法,可以获取到当前线程的类加载器对象。getResource() 【获取资源】这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源。*/String path = Thread.currentThread().getContextClassLoader().getResource("classinfo2.properties").getPath(); // 这种方式获取文件绝对路径是通用的。// 采用以上的代码可以拿到一个文件的绝对路径。// /C:/Users/Administrator/IdeaProjects/javase/out/production/chapter25/classinfo2.propertiesSystem.out.println(path);// 获取db.properties文件的绝对路径(从类的根路径下作为起点开始)String path2 = Thread.currentThread().getContextClassLoader().getResource("com/bjpowernode/java/bean/db.properties").getPath();System.out.println(path2);}
}

以流的形式直接返回

import java.io.FileReader;
import java.io.InputStream;
import java.util.Properties;public class IoPropertiesTest {public static void main(String[] args) throws Exception{// 获取一个文件的绝对路径了!!!!!/*String path = Thread.currentThread().getContextClassLoader().getResource("classinfo2.properties").getPath();FileReader reader = new FileReader(path);*/// 直接以流的形式返回。InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("classinfo2.properties");Properties pro = new Properties();pro.load(reader);reader.close();// 通过key获取valueString className = pro.getProperty("className");System.out.println(className);}
}

资源绑定器

java.util包下提供了一个资源绑定器,便于获取属性配置文件中的内容。
使用以下这种方式的时候,属性配置文件xxx.properties必须放到类路径下
并且在写路径的时候,路径后面的扩展名不能写!

import java.util.ResourceBundle;public class ResourceBundleTest {public static void main(String[] args) {// 资源绑定器,只能绑定xxx.properties文件。并且这个文件必须在类路径下。文件扩展名也必须是properties// 并且在写路径的时候,路径后面的扩展名不能写。//ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");ResourceBundle bundle = ResourceBundle.getBundle("com/bjpowernode/java/bean/db");String className = bundle.getString("className");System.out.println(className);}
}

类加载器

关于JDK中自带的类加载器:
1、什么是类加载器?
专门负责加载类的命令/工具。
ClassLoader

2、JDK中自带了3个类加载器
启动类加载器:rt.jar(父加载器)
扩展类加载器:ext/*.jar(母加载器)
应用类加载器:classpath

3、假设有这样一段代码:
String s = “abc”;

代码在开始执行之前,会将所需要的类全部加载到JVM当中。
通过类加载器加载,看到以上代码类加载器会找到String.class文件,找到就加载,那么是怎么进行加载的呢?

首先通过“启动类加载器”加载。
注意:启动类加载器专门加载:C:\Program Files\Java\jdk1.8.0_201\jre\lib\rt.jar
rt.jar中都是JDK最核心的类库

如果通过“启动类加载器”加载不到的时候,
会通过“扩展类加载器”加载。
注意:扩展类加载器专门加载:C:\Program Files\Java\jdk1.8.0_201\jre\lib\ext*.jar

如果“扩展类加载器”没有加载到,那么
会通过“应用类加载器”加载。
注意:应用类加载器专门加载:classpath中的类

双亲委派机制

java中为了保证类加载的安全,使用了双亲委派机制。
优先从启动类加载器中加载,这个称为“父”,“父”无法加载到,再从扩展类加载器中加载,这个称为“母”。双亲委派。如果都加载不到,才会考虑从应用类加载器中加载。直到加载到为止。

获取Field

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;public class ReflectTest05 {public static void main(String[] args) throws Exception{// 获取整个类Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");//com.bjpowernode.java.bean.StudentString className = studentClass.getName();System.out.println("完整类名:" + className);String simpleName = studentClass.getSimpleName();System.out.println("简类名:" + simpleName);// 获取类中所有的public修饰的FieldField[] fields = studentClass.getFields();System.out.println(fields.length); // 测试数组中只有1个元素// 取出这个FieldField f = fields[0];// 取出这个Field它的名字String fieldName = f.getName();System.out.println(fieldName);// 获取所有的FieldField[] fs = studentClass.getDeclaredFields();System.out.println(fs.length); // 4System.out.println("==================================");// 遍历for(Field field : fs){// 获取属性的修饰符列表int i = field.getModifiers(); // 返回的修饰符是一个数字,每个数字是修饰符的代号!!!System.out.println(i);// 可以将这个“代号”数字转换成“字符串”吗?String modifierString = Modifier.toString(i);System.out.println(modifierString);// 获取属性的类型Class fieldType = field.getType();//String fName = fieldType.getName();String fName = fieldType.getSimpleName();System.out.println(fName);// 获取属性的名字System.out.println(field.getName());}}
}// 反射属性Field
public class Student {// Field翻译为字段,其实就是属性/成员// 4个Field,分别采用了不同的访问控制权限修饰符private String name; // Field对象protected int age; // Field对象boolean sex;public int no;public static final double MATH_PI = 3.1415926;
}

通过反射机制反编译一个类的属性Field

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;public class ReflectTest06 {public static void main(String[] args) throws Exception{// 创建这个是为了拼接字符串。StringBuilder s = new StringBuilder();//Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");Class studentClass = Class.forName("java.lang.Thread");s.append(Modifier.toString(studentClass.getModifiers()) + " class " + studentClass.getSimpleName() + " {\n");Field[] fields = studentClass.getDeclaredFields();for(Field field : fields){s.append("\t");s.append(Modifier.toString(field.getModifiers()));s.append(" ");s.append(field.getType().getSimpleName());s.append(" ");s.append(field.getName());s.append(";\n");}s.append("}");System.out.println(s);}
}

*通过反射机制访问对象属性

必须掌握
怎么通过反射机制访问一个java对象的属性?
给属性赋值 set
获取属性的值 get

可以访问私有属性吗?
可以。打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!)
nameField.setAccessible(true); //这样设置完之后在外部也是可以访问private的。

import com.bjpowernode.java.bean.Student;import java.lang.reflect.Field;public class ReflectTest07 {public static void main(String[] args) throws Exception{// 我们不使用反射机制,怎么去访问一个对象的属性呢?Student s = new Student();// 给属性赋值s.no = 1111; //三要素:给s对象的no属性赋值1111//要素1:对象s//要素2:no属性//要素3:1111// 读属性值// 两个要素:获取s对象的no属性的值。System.out.println(s.no);// 使用反射机制,怎么去访问一个对象的属性。(set get)Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");Object obj = studentClass.newInstance(); // obj就是Student对象。(底层调用无参数构造方法)// 获取no属性(根据属性的名称来获取Field)Field noFiled = studentClass.getDeclaredField("no");// 给obj对象(Student对象)的no属性赋值/*虽然使用了反射机制,但是三要素还是缺一不可:要素1:obj对象要素2:no属性要素3:2222值注意:反射机制让代码复杂了,但是为了一个“灵活”,这也是值得的。*/noFiled.set(obj, 22222); // 给obj对象的no属性赋值2222// 读取属性的值// 两个要素:获取obj对象的no属性的值。System.out.println(noFiled.get(obj));// 可以访问私有的属性吗?Field nameField = studentClass.getDeclaredField("name");// 打破封装(反射机制的缺点:打破封装,可能会给不法分子留下机会!!!)// 这样设置完之后,在外部也是可以访问private的。nameField.setAccessible(true);// 给name属性赋值nameField.set(obj, "jackson");// 获取name属性的值System.out.println(nameField.get(obj));}
}

可变长度参数

int… args 这就是可变长度参数
语法是: 类型… (注意:一定是3个点。)

1、可变长度参数要求的参数个数是:0~N个。
2、可变长度参数在参数列表中必须在最后一个位置上,而且可变长度参数只能有1个
3、可变长度参数可以当做一个数组来看待。

public class ArgsTest {public static void main(String[] args) {m();m(10);m(10, 20);// 编译报错//m("abc");m2(100);m2(200, "abc");m2(200, "abc", "def");m2(200, "abc", "def", "xyz");m3("ab", "de", "kk", "ff");String[] strs = {"a","b","c"};// 也可以传1个数组m3(strs);// 直接传1个数组m3(new String[]{"我","是","中","国", "人"}); //没必要m3("我","是","中","国", "人");}public static void m(int... args){System.out.println("m方法执行了!");}//报错//public static void m2(int... args2, String... args1){}// 必须在最后,只能有1个。public static void m2(int a, String... args1){}public static void m3(String... args){//args有length属性,说明args是一个数组!// 可以将可变长度参数当做一个数组来看。for(int i = 0; i < args.length; i++){System.out.println(args[i]);}}
}

反射Method

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;public class ReflectTest08 {public static void main(String[] args) throws Exception{// 获取类了Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");// 获取所有的Method(包括私有的!)Method[] methods = userServiceClass.getDeclaredMethods();//System.out.println(methods.length); // 2// 遍历Methodfor(Method method : methods){// 获取修饰符列表System.out.println(Modifier.toString(method.getModifiers()));// 获取方法的返回值类型System.out.println(method.getReturnType().getSimpleName());// 获取方法名System.out.println(method.getName());// 方法的修饰符列表(一个方法的参数可能会有多个。)Class[] parameterTypes = method.getParameterTypes();for(Class parameterType : parameterTypes){System.out.println(parameterType.getSimpleName());}}}
}public class UserService {/*** 登录方法* @param name 用户名* @param password 密码* @return true表示登录成功,false表示登录失败!*/public boolean login(String name,String password){if("admin".equals(name) && "123".equals(password)){return true;}return false;}// 可能还有一个同名login方法// java中怎么区分一个方法,依靠方法名和参数列表。public void login(int i){}/*** 退出系统的方法*/public void logout(){System.out.println("系统已经安全退出!");}
}

*反射机制调用方法

重点:必须掌握,通过反射机制怎么调用一个对象的方法?

反射机制:让代码很具有通用性,可变化的内容都是写到配置文件当中,将来修改配置文件之后,创建的对象不一样了,调用的方法也不同了,但是java代码不需要做任何改动。这就是反射机制的魅力。

import com.bjpowernode.java.service.UserService;import java.lang.reflect.Method;public class ReflectTest10 {public static void main(String[] args) throws Exception{// 不使用反射机制,怎么调用方法// 创建对象UserService userService = new UserService();// 调用方法/*要素分析:要素1:对象userService要素2:login方法名要素3:实参列表要素4:返回值*/boolean loginSuccess = userService.login("admin","123");//System.out.println(loginSuccess);System.out.println(loginSuccess ? "登录成功" : "登录失败");// 使用反射机制来调用一个对象的方法该怎么做?Class userServiceClass = Class.forName("com.bjpowernode.java.service.UserService");// 创建对象Object obj = userServiceClass.newInstance();// 获取MethodMethod loginMethod = userServiceClass.getDeclaredMethod("login", String.class, String.class);//Method loginMethod = userServiceClass.getDeclaredMethod("login", int.class);// 调用方法// 调用方法有几个要素? 也需要4要素。// 反射机制中最最最最最重要的一个方法,必须记住。/*四要素:loginMethod方法obj对象"admin","123" 实参retValue 返回值*/Object retValue = loginMethod.invoke(obj, "admin","123123");System.out.println(retValue);}
}

反射Constructor

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;/*
反编译一个类的Constructor构造方法。*/
public class ReflectTest11 {public static void main(String[] args) throws Exception{StringBuilder s = new StringBuilder();Class vipClass = Class.forName("java.lang.String");s.append(Modifier.toString(vipClass.getModifiers()));s.append(" class ");s.append(vipClass.getSimpleName());s.append("{\n");// 拼接构造方法Constructor[] constructors = vipClass.getDeclaredConstructors();for(Constructor constructor : constructors){//public Vip(int no, String name, String birth, boolean sex) {s.append("\t");s.append(Modifier.toString(constructor.getModifiers()));s.append(" ");s.append(vipClass.getSimpleName());s.append("(");// 拼接参数Class[] parameterTypes = constructor.getParameterTypes();for(Class parameterType : parameterTypes){s.append(parameterType.getSimpleName());s.append(",");}// 删除最后下标位置上的字符if(parameterTypes.length > 0){s.deleteCharAt(s.length() - 1);}s.append("){}\n");}s.append("}");System.out.println(s);}
}

通过反射机制调用构造方法

import com.bjpowernode.java.bean.Vip;import java.lang.reflect.Constructor;public class ReflectTest12 {public static void main(String[] args) throws Exception{// 不使用反射机制怎么创建对象Vip v1 = new Vip();Vip v2 = new Vip(110, "zhangsan", "2001-10-11", true);// 使用反射机制怎么创建对象呢?Class c = Class.forName("com.bjpowernode.java.bean.Vip");// 调用无参数构造方法Object obj = c.newInstance();System.out.println(obj);// 调用有参数的构造方法怎么办?// 第一步:先获取到这个有参数的构造方法Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);// 第二步:调用构造方法new对象Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);System.out.println(newObj);// 获取无参数构造方法Constructor con2 = c.getDeclaredConstructor();Object newObj2 = con2.newInstance();System.out.println(newObj2);}
}

获取父类和父接口

public class ReflectTest13 {public static void main(String[] args) throws Exception{// String举例Class stringClass = Class.forName("java.lang.String");// 获取String的父类Class superClass = stringClass.getSuperclass();System.out.println(superClass.getName());// 获取String类实现的所有接口(一个类可以实现多个接口。)Class[] interfaces = stringClass.getInterfaces();for(Class in : interfaces){System.out.println(in.getName());}}
}

注解

注解,或者叫做注释类型,英文单词是:Annotation

注解Annotation是一种引用数据类型。编译之后也是生成xxx.class文件。

怎么自定义注解呢?语法格式?
[修饰符列表] @interface 注解类型名{

}

注解怎么使用,用在什么地方?
第一:注解使用时的语法格式是:
@注解类型名
第二:注解可以出现在类上、属性上、方法上、变量上等…、
注解还可以出现在注解类型上。

JDK内置了哪些注解

java.lang包下的注释类型:
掌握:
Deprecated 用@Deprecated 注释的程序元素。
不鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择。

掌握:
Override 表示一个方法声明打算重写超类中的另一个方法声明。

不用掌握:
SuppressWarnings 指示应该在注释元素(以及包含在该注释元素中的所有程序元素)中取消显示指定的编译器警告。

Override注解

关于JDK lang包下的Override注解
源代码:
public @interface Override {
}

标识性注解,给编译器做参考的。编译器看到方法上有这个注解的时候,编译器会自动检查该方法是否重写了父类的方法。如果没有重写,报错。

这个注解只是在编译阶段起作用,和运行期无关!

// @Override这个注解只能注解方法。
// @Override这个注解是给编译器参考的,和运行阶段没有关系。
// 凡是java中的方法带有这个注解的,编译器都会进行编译检查,如果这个方法不是重写父类的方法,编译器报错。//@Override
public class AnnotationTest02 {//@Overrideprivate int no;@Overridepublic String toString() {return "toString";}}

元注解

什么是元注解?
用来标注“注解类型”的“注解”,称为元注解。

常见的元注解有哪些?
Target
Retention

关于Target注解:
这是一个元注解,用来标注“注解类型”的“注解”。
这个Target注解用来标注“被标注的注解”可以出现在哪些位置上。

@Target(ElementType.METHOD):表示“被标注的注解”只能出现在方法上。
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}):表示该注解可以出现在:构造方法上,字段上,局部变量上,方法上,…,类上。

关于Retention注解:
这是一个元注解,用来标注“注解类型”的“注解”。
这个Retention注解用来标注“被标注的注解”最终保存在哪里。

@Retention(RetentionPolicy.SOURCE):表示该注解只被保留在java源文件中。
@Retention(RetentionPolicy.CLASS):表示该注解被保存在class文件中。
@Retention(RetentionPolicy.RUNTIME):表示该注解被保存在class文件中,并且可以被反射机制所读取。

注解中定义属性

public @interface MyAnnotation {/*** 我们通常在注解当中可以定义属性,以下这个是MyAnnotation的name属性。* 看着像1个方法,但实际上我们称之为属性name。* @return*/String name();/*颜色属性*/String color();/*年龄属性*/int age() default 25; //属性指定默认值}
public class MyAnnotationTest {// 报错的原因:如果一个注解当中有属性,那么必须给属性赋值。(除非该属性使用default指定了默认值。)/*@MyAnnotationpublic void doSome(){}*///@MyAnnotation(属性名=属性值,属性名=属性值,属性名=属性值)//指定name属性的值就好了。@MyAnnotation(name = "zhangsan", color = "红色")public void doSome(){}
}

属性名是value时可以省略

如果一个注解的属性的名字时value,并且只有一个属性的话,在使用的时候,该属性名可以省略。

public class MyAnnotationTest {// 报错原因:没有指定属性的值。/*@MyAnnotationpublic void doSome(){}*/@MyAnnotation(value = "hehe")public void doSome(){}@MyAnnotation("haha")public void doOther(){}
}
public @interface MyAnnotation {/*指定一个value属性。*/String value();//String email();
}

属性可以是哪一种类型

注解当中的属性的类型可以是:
byte short int long float double boolean char String Class 枚举类型
以及以上每一种的数组形式。

属性是一个数组

public enum Season {SPRING,SUMMER,AUTUMN,WINTER
}
public @interface OtherAnnotation {/*年龄属性*/int age();/*邮箱地址属性,支持多个*/String[] email();/*** 季节数组,Season是枚举类型* @return*/Season[] seasonArray();
}
public class OtherAnnotationTest {// 数组是大括号@OtherAnnotation(age = 25, email = {"zhangsan@123.com", "zhangsan@sohu.com"}, seasonArray = Season.WINTER)public void doSome(){}// 如果数组中只有1个元素:大括号可以省略。@OtherAnnotation(age = 25, email = "zhangsan@123.com", seasonArray = {Season.SPRING, Season.SUMMER})public void doOther(){}
}

反射注解

通过反射获取注解对象属性的值:
例1:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;//只允许该注解可以标注类、方法
@Target({ElementType.TYPE, ElementType.METHOD})
// 希望这个注解可以被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {/*value属性。*/String value() default "北京大兴区";
}
@MyAnnotation("上海浦东区")
public class MyAnnotationTest {//@MyAnnotationint i;//@MyAnnotationpublic MyAnnotationTest(){}@MyAnnotationpublic void doSome(){//@MyAnnotationint i;}
}
public class ReflectAnnotationTest {public static void main(String[] args) throws Exception{// 获取这个类Class c = Class.forName("com.bjpowernode.java.annotation5.MyAnnotationTest");// 判断类上面是否有@MyAnnotation//System.out.println(c.isAnnotationPresent(MyAnnotation.class)); // trueif(c.isAnnotationPresent(MyAnnotation.class)){// 获取该注解对象MyAnnotation myAnnotation = (MyAnnotation)c.getAnnotation(MyAnnotation.class);//System.out.println("类上面的注解对象" + myAnnotation); // @com.bjpowernode.java.annotation5.MyAnnotation()// 获取注解对象的属性怎么办?和调接口没区别。String value = myAnnotation.value();System.out.println(value);}// 判断String类上面是否存在这个注解Class stringClass = Class.forName("java.lang.String");System.out.println(stringClass.isAnnotationPresent(MyAnnotation.class)); // false}
}

例2:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {/*username属性*/String username();/*password属性*/String password();
}
import java.lang.reflect.Method;public class MyAnnotationTest {@MyAnnotation(username = "admin", password = "456456")public void doSome(){}public static void main(String[] args) throws Exception{// 获取MyAnnotationTest的doSome()方法上面的注解信息。Class c = Class.forName("com.bjpowernode.java.annotation6.MyAnnotationTest");// 获取doSome()方法Method doSomeMethod = c.getDeclaredMethod("doSome");// 判断该方法上是否存在这个注解if(doSomeMethod.isAnnotationPresent(MyAnnotation.class)) {MyAnnotation myAnnotation = doSomeMethod.getAnnotation(MyAnnotation.class);System.out.println(myAnnotation.username());System.out.println(myAnnotation.password());}}}

注解在开发中有什么用

需求:
假设有这样一个注解,叫做:@MustHasIdPropertyAnnotation
这个注解只能出现在类上面,当这个类上有这个注解的时候,
要求这个类中必须有一个int类型的id属性。如果没有这个属性
就报异常。如果有这个属性则正常执行!

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;// 表示这个注解只能出现在类上面
@Target(ElementType.TYPE)
// 该注解可以被反射机制读取到
@Retention(RetentionPolicy.RUNTIME)
public @interface MustHasIdPropertyAnnotation {}
/*
自定义异常*/
public class HasNotIdPropertyException extends RuntimeException {public HasNotIdPropertyException(){}public HasNotIdPropertyException(String s){super(s);}
}
@MustHasIdPropertyAnnotation
public class User {int id;String name;String password;
}
import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception{// 获取类Class userClass = Class.forName("com.bjpowernode.java.annotation7.User");// 判断类上是否存在Id注解if(userClass.isAnnotationPresent(MustHasIdPropertyAnnotation.class)){// 当一个类上面有@MustHasIdPropertyAnnotation注解的时候,要求类中必须存在int类型的id属性// 如果没有int类型的id属性则报异常。// 获取类的属性Field[] fields = userClass.getDeclaredFields();boolean isOk = false; // 给一个默认的标记for(Field field : fields){if("id".equals(field.getName()) && "int".equals(field.getType().getSimpleName())){// 表示这个类是合法的类。有@Id注解,则这个类中必须有int类型的idisOk = true; // 表示合法break;}}// 判断是否合法if(!isOk){throw new HasNotIdPropertyException("被@MustHasIdPropertyAnnotation注解标注的类中必须要有一个int类型的id属性!");}}}
}

【JavaSE进阶(下)】自学笔记 记得收藏时时回顾相关推荐

  1. 【JavaSE进阶(上)】自学笔记 记得收藏时时回顾

    final关键字 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y4clTw5g-1649075023636)(https://cdn.nlark.com/yuque/ ...

  2. Java自学笔记——Java面向对象——04.抽象类、接口、内部类

    Java 面向对象各节 Java自学笔记--Java面向对象--01.回顾方法 Java自学笔记--Java面向对象--02.构造器.类和对象 Java自学笔记--Java面向对象--03.封装.继承 ...

  3. JavaSE自学笔记013_Real(抽象类、接口、两种设计模式)

    JavaSE自学笔记013_Real(抽象类.接口) 一.基本概述 (一)抽象类(关键字:abstract) //抽象方法 public abstract class Animal{abstract ...

  4. JavaSE自学笔记Real_004

    JavaSE自学笔记Real_004 封装 Private get set public class Fengzhuang {public static void main(String[] args ...

  5. JavaSE自学笔记Real_008(多线程基础)

    JavaSE自学笔记Real_008(多线程基础) 线程的优先级设置(priority) 线程的优先级用数字表示,范围是1到10(在范围之外会报错) Thread.MIN_PRIORITY = 1 T ...

  6. JavaSE进阶学习笔记-目录汇总(待完成)

    声明:此博客来自于黑马程序员学习笔记,并非商用,仅仅是为了博主个人日后学习复习用,如有冒犯,请联系qq208820388立即删除博文,最后,来跟我一起喊黑马牛逼黑马牛逼黑马牛逼 JavaSE进阶学习笔 ...

  7. JavaSE自学笔记016_Real(多线程)

    JavaSE自学笔记016_Real(多线程) 一.进程与线程 1.进程 一个正在执行中的程序叫做一个进程.系统会为了这个进程发配独立的[内存资源],进程是程序的依次执行过程,他有着自己独立的生命周期 ...

  8. qml自学笔记------自己写类似于劲舞团的按键小游戏(下)

    接上篇<qml自学笔记------自己写类似于劲舞团的按键小游戏(中)> 第四部分 PauseButton.qml 和 RestartButton.qml 第四部分其实就是两个按键,一个是 ...

  9. JavaSE 进阶 - 第23章 IO流

    JavaSE 进阶 - 第23章 IO流 1.IO流,什么是IO? 2.IO流的分类 3.流应该怎样学习? 4.java IO流的四大家族 5.java.io包下需要掌握的16个流 5.1 FileI ...

最新文章

  1. 祝51CTO 生日快乐
  2. SVN - 简单使用手册
  3. Ansible系列之roles使用说明
  4. @PathVariable、@RequestParam、@RequestBody注解
  5. CSS基本知识之盒子模型
  6. linux中如何批量修改文件名
  7. python游戏代码运行不了_用Python写游戏,不到十分钟就学会了
  8. HDU2084 数塔【DP】
  9. HttpClient 发送请求和参数
  10. java给图片增加水印(图片水印,文字水印)
  11. 主流数据库管理系统介绍
  12. php保存pdf旋转90度,如何将PDF图片旋转90度,盘点这个小方法
  13. html 转盘素材,jQuery指针不动转盘动的Rotate转盘插件
  14. mysql 3306无法访问_Mysql 3306端口无法被远程机器访问
  15. Java输出九九乘法表(99乘法)
  16. 无线蓝牙耳机什么牌子好?适合学生党的蓝牙耳机性价比排行榜
  17. onlyoffice转换html,CentOS7安装onlyoffice实现word的在线预览和编辑
  18. 【宇宙最强编辑器VS Code】(二)美化VS Code(更换主题和文件图标主题、设置背景图片)
  19. CI(CodeIgniter)框架入门
  20. 2021年金融机构贷款投向统计报告

热门文章

  1. 【UOJ 429】串串划分(Runs)(容斥)+ 有关 Lyndon Tree 及其应用的小小记录
  2. 【Rust日报】2020-10-12 蜜月期之后的 Rust
  3. autojs之桌面快捷方式
  4. 计算机辅助设计 Altium Designer
  5. 开发的都用高科技,管理的都在过家家。。。
  6. 快易拍联合新浪微公益一起为贫困孩子过温暖新年吧!
  7. 《财经问题研究》投稿经验分享
  8. 解决报错 ,启动类启动不起来,关于swagger
  9. 【喜讯】零伽壹入驻陀螺财经产业区块链项目库
  10. 驾照考试——科目二(跑圈)