1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > JavaSE重点之集合 IO 多线程

JavaSE重点之集合 IO 多线程

时间:2023-11-27 11:20:05

相关推荐

JavaSE重点之集合 IO 多线程

java基础

1. 集合1.1 概述1.2 Collection集合常用方法:迭代器List集合ArrayList集合LinkedListVector集合泛型HashSet集合TreeSet集合1.3 Map集合HashMapHashtablePropertiesTreeMapCollections集合工具类1.4 面试题2. IO流2.1 概述2.2 流文件专属转换流缓冲流数据流标准输出流对象专属流序列化流IO和Properties联合使用2.3 File类拷贝目录3. 多线程3.1 概述3.2 实现线程的两种方式3.3 线程的生命周期3.4 相关方法3.5 线程调度3.6 线程安全(重点)同步代码块synchronized死锁现象3.7 守护线程和定时器3.8 实现线程的第三种方式3.9 生产者消费者模型4. 遗漏的基础4.1 static关键字4.2 多态4.3 抽象类和接口

1. 集合

1.1 概述

数组其实就是一个集合,集合实际上就是一个容器,可以来容纳其他类型的数据。

在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个个数据展现出来。

集合不能直接存储基本数据类型,也不能直接存储java对象,集合当中存储的都是java对象的内存地址。集合中任何时候存储都是“引用”。

不同的集合,底层对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构中。

重点掌握:什么情况下选择哪一种合适的集合去使用。

集合的继承结构图:

总结:

ArrayList:底层是数组。

LinkedList:底层是双向链表

Vector:底层是数组,线程安全,效率较低,使用较少。

HashSet:底层是HashMap,放到HashSet集合中的元素等同于放到HashMap集合的key部分了。

TreeSet:底层是TreeMap,放到TreeSet集合中的元素等同于放到了TreeMap集合key部分了。

HashMap:底层是哈希表。

Hashtable:底层也是哈希表,只不过是线程安全的,效率较低,使用较少。

Properties:是线程安全的,并且key和value只能存储字符串String。

TreeMap:底层是二叉树。TreeMap集合的key可以自动按照大小顺序排序。

List集合存储元素的特点:有序可重复。(有序是指存进和取出的顺序相同,每一个元素都有下标)

Set集合存储元素的特点:无序不可重复。

SortedSet(SortedMap)集合存储元素的特点:无序不重复,按照元素大小进行排序。

1.2 Collection集合

如果没有使用泛型,Collection可以存储Object的所有子类型;使用泛型之后,只能存储某个具体的类型。

常用方法:

add、size、clear、contains、remove、isEmpty、toArray、Iterator

注意:contains方法和remove方法,底层调用了equals方法。

//contains方法是用来判断是否包含某个元素Collection c = new ArrayList();String s1 = new String("abc");c.add(s1);String x = new String("abc");c.contains(x); //虽然没有用add方法添加x,但这里还是返回true。//因为底层调用equals方法,比较的是内容User u1 = new User("jack");c.add(u1);User u2 = new User("jack");c.contains(u2); //如果User没有重写equals方法,返回false;重写了equals方法,返回true。

没有重写equals方法,底层会进行内存的比较。

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

String s1 = new String("hello");c.add(s1);String s2 = new String("hello");c.remove(s2); //上面添加的s1,这里删除s2sout(c.size); //返回0

深入Collection集合的contains方法

contains方法是用来判断集合中是否包含某个元素的方法。如果包含返回true,如果不包含返回false。

它在底层是如何判断集合中是否包含某个元素的呢?

它是调用了equals方法进行比对的,

equals方法返回true,就表示包含这个元素。

public static void main(String[] args) {Collection c = new ArrayList();//向集合中存储元素String s1 = new String("abc");c.add(s1);String s2 = new String("def");c.add(s2);String x = new String("abc"); //没往集合中存储x// 问:集合中是否包含x?System.out.println(c.contains(x)); //这句相当是在判断 c集合包不包含"abc"}

contains()比较对象:

如果对象没有重写equals方法,即使值相同也返回false,因为contains会调用equals方法,而对象没重写equals方法,则会去调用Object类的equals方法,Object类的equals方法是用 “==” 进行比较的。

Collection c = new ArrayList();User u1 = new User("jack");c.add(u1);User u2 = new User("jack");// user对象没有重写equals之前,这个结果为falseSystem.out.println(c.contains(u2));

关于集合中remove的操作

remove方法也调用了equals方法,下面删掉s2,集合中s1也没了。

Collection c = new ArrayList();String s1 = new String("hello");c.add(s1);String s2 = new String("hello");c.remove(s2);System.out.println(c.size()); // 0

迭代器要在操作完集合后获取。集合的结构只要发生改变,迭代器必须重新获取。

所以,在用迭代器遍历集合的时候,不能调用对象的remove方法去删除集合中的元素,不然会出现异常。

要用迭代器中的remove方法删除。

Collection c = new ArrayList();c.add("abc");c.add("efg");Iterator it = c.iterator();while(it.hasNext()) {Object o = it.next();it.remove(); //删除当前指向的元素}

使用对象的remove删除会出现异常,原因是集合中元素删除了,没有更新迭代器。

用迭代器去删除,会自动更新迭代器,并且更新集合。

迭代器

这里的迭代方式,在所有的Collection以及子类中可以使用。

三个方法:boolean hasNext()、object next()、void remove()。

hasNext():是否还有元素可以迭代

next():让迭代器前进一位,并且拿到元素。

boolean hasNext = it.hasNext();如果返回true,表示还有元素可以迭代;返回false表示没有更多的元素可以迭代了。

Collection c = new ArrayList();Iterator it = c.iterator(); //拿到迭代器对象while(it.hasNext()) {Object obj = it.next();sout(obj);}

List集合

list集合:有序可重复。是Collection接口的子接口,所以可以使用Collection的方法,它也有自己“特色”的方法。

特有的常用方法

void add(int index, Object element)、Object get(int index)

int indexOf(Object o)、int lastIndexOf(Object o)、Object remove(int index)

Object set(int index, Object element)

List myList = new ArrayList();

ArrayList集合

底层采用的数组这个数据结构。

优点:检索效率比较高。

缺点:随机增删元素效率比较低(但在数组末尾增删,效率不受影响);数组无法存储大数据量。

ArrayList是非线程安全的,线程安全的集合效率会低一点。

ArrayList集合初始化容量是10,可以在创建的时候指定容量new ArrayList(20);

ArrayList集合用的比较多,因为往数组末尾增删元素,效率不受影响,而且我们检索/查找某个元素的操作比较多。

ArrayList集合扩容

源码中说的是:新容量 = 原容量 + 原容量>>1(位运算),也就是新容量 = 原容量 + 原容量/2

原容量的1.5倍。

ArrayList底层是数组,数组扩容效率比较低,所以要尽可能少的扩容。建议在使用ArrayList集合的时候预估记元素的个数,给定一个初始化容量。ArrayList集合的另一个构造方法

List list1 = new ArrayList(); //底层用的数组List list2 = new ArrayList(100);Collection c = new HashSet();c.add(100);c.add(230);c.add(1320);//通过这个构造方法可以将HashSet集合转换成List集合List list3 = new ArrayList(c);

将ArrayList变成线程安全

List list = new ArrayList(); //非线程安全Collections.synchronizedList(list);list.add("dfsdfs");...

LinkedList

LinkedList底层采用双向链表数据结构。

单向链表:

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

优点:由于链表上的元素在空间存储上内存地址不连续,随机增删元素效率较高(因为链表增删不涉及大量元素的位移)。在以后开发中,如果遇到随机增删集合中元素的业务比较多,建议使用LinkedList。

缺点:查询效率较低,每一次查找某个元素的时候都需要从头节点开始往下遍历。

ArrayList:把检索发挥到极致。(末尾添加元素,效率很高)

LinkedList:把随机增删发挥到极致。

加元素,往往是加在集合末尾,所以ArrayList用的比LinkedList多。

注意: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);sout(obj);}

1)单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找

2)单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除节点,总是找到temp,temp是待删除节点的前一个节点。

Vector集合

底层也是一个数组,初始化容量为10。

扩容:扩容之后是原来容量的2倍。

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

泛型

泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。

使用泛型的好处:

集合中存储的元素类型统一了。

从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”。

缺点:

导致集合中存储的元素缺乏多样性。

大多数业务中,集合中元素的类型还是统一的,所以这种泛型特性被大家所认可。

List<Animal> myList = new ArrayList<Animal>(); //使用泛型,只能存储指定的类型Cat c = new Cat();Bird b = new Bird();myList.add(c);myList.add(b);Iterator<Animal> it = myList.iterator();while(it.hasNext()){//这里不用再进行强制类型转换了,直接调用。Animal a = it.next(); //迭代器取出来就是Animal类型a.move();/*if(obj instanceof Animal) {Animal a = (Animal)obj;a.move();}*///不过调用子类的特有方法,还是要转型的}

在JDK1.8之后,引入了自动类型推断机制:

List<Animal> list = new ArrayList<>();就是后面尖括号可以省略类型。

自定义泛型

<>中的T是一个标识符,随便写。

java源码中经常出现的是:<E><T>

public class Test<T> {public T get() {return null;}public void dosome(T o) {sout(o);}public static void main(String[] args) {Test<String> t = new Test<>();String s = t.get(); //这里只能返回String类型,因为设置了泛型Test<String> t2 = new Test<>();t2.dosome("abc");}}

HashSet集合

存和取的顺序不一样;不可重复;

放到HashSet集合中的元素实际上是放到了HashMap集合的key部分了。

TreeSet集合

无序不可重复,但是存储的元素可以自动按照大小顺序排序。(称为可排序集合)

无序指的是:没有下标,存和取的顺序可能不同。

1.3 Map集合

Map集合以key和value的方式存储数据,key和value都是引用数据类型。

常用方法

V put(key, value):向Map集合中添加键值对、

V get(Object key):通过key获取value、

void clear():清空map集合、

boolean isEmpty():判断集合中元素个数是否为0、

boolean containsKey(Object key):是否包含某个key、

boolean containsValue(Object value):判断Map中是否包含某个value、

Set<K> keySet():获取集合所有的key 、

V remove(Object key):通过key删除键值对、

int size():获取集合中键值对的个数、

Collection<V> values():获取集合中所有的value、

Set<Map.Entry<K,V>> entrySet():将集合转换成Set集合,1=zhangsan

Map集合的遍历

方式一:先拿到Map集合所有的key,根据key,获取value。

Map<Integer,String> map = new HashMap<>();map.put(1,"zhangsan");...Set<Integer> keys = map.keySet(); //拿到所有keyIterator<Integer> it = keys.iterator(); //拿到Set集合的迭代器while(it.hasNext()) {Integer key = it.next(); //拿到一个keyString value = map.get(key); //根据key获取valuesout(key + "=" + value);}

不想用迭代器,用增强for

Set<Integer> keys = map.keySet(); //拿到所有keyfor(Integer key : keys) {sout(key + "=" + map.get(key));}

方式二:Set<Map.Entry<K,V>> entrySet(),将map集合转换成set集合

//先将Map集合转换成Set集合Set<Map.Entry<Integer,String>> set = map.entrySet();//拿到迭代器Iterator<Map.Entry<Integer,String>> it2 = set.iterator();//通过迭代器遍历set集合中的元素,元素对象是Map.Entry<Integer,String>类型while(it2.hasNext()) {Map.Entry<Integer,String> node = it2.next();//拿到对象中的keyInteger key = node.getKey();//拿到对象中的valueString value = node.getValue();sout(key + "=" +value);}

用增强for

for(Map.Entry<Integer,String> node : set) {sout(node.getKey() + "--->" + node.getValue());}

第二种方式效率比较高,因为获取key和value都是直接从node对象找中获取的属性值。

HashMap

HashMap集合底层是 哈希表/散列表 的数据结构,非线程安全。

哈希表数据结构

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

数组:在查询方面效率很高,随机增删方面效率很低。

单向链表:在随机增删方面效率较高,在查询方面效率很低。

哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。

HashMap集合底层的源代码:

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

哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。

重点:要知道put怎么存,get怎么取。

为什么哈希表的随机增删,以及查询效率都很高?

因为增删是在链表上完成的,查询也不需要全都扫描,只需要部分扫描。

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

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

假设HashCode()方法返回值固定为某个值,那么底层哈希表会变成纯单向链表;假设HashCode()方法方法返回值都设定为不一样的值,那么底层哈希表就成一维数组了,不会在数组元素下挂链表(散列分布不均匀)。散列分布均匀,需要你重写hashCode()方法时有一定的技巧。

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

向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,再可能调用equals方法(equals可能调用也可能不调用)。数组下标位置上如果是null,equals不需要执行。

一个类如果equals方法重写了,那么hashCode()方法必须重写。

HashMap集合的默认初始化容量是16,默认加载因子是0.75

默认加载因子:集合底层数组的容量达到75%的时候,数组开始扩容。

重点

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

最终结论

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

JDK8之后,如果哈希表单向链表中元素 超过8个,单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。

Hashtable

线程安全。底层也是哈希表数据结构。

初始化容量是11,默认加载因子是:0.75f,扩容是:原容量*2 + 1

HashTable与HashMap比较

HashMap的key和value可以为null

Map map = new HashMap();map.put(null,null);

而Hashtable的key和value不能为null

Properties

Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。Properties被称为属性类对象。Properties是线程安全的。

掌握两个方法,一个存,一个取。

Properties pro = new Properties();pro.setProperty("url","jdbc:mysql://localhost:3306/ld");pro.getProperty("url");

TreeMap

TreeMap集合底层是一个二叉树。TreeSet集合底层实际上是一个TreeMap,放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。

TreeSet集合中的元素,无序不可重复,但是可以按照元素的大小顺序自动排序(升序)。

TreeSet对自定义类型的排序

自定义类型要实现Comparable接口,不然会出现异常,因为TreeSet要进行排序,如果自定义类型没有指定对象之间的比较规则,就会出错。

Customer c1 = new Customer(32);Customer c2 = new Customer(20);Customer c3 = new Customer(30);Customer c4 = new Customer(25);TreeSet<Customer> customers = new TreeSet<>();customers.add(c1);customers.add(c2);customers.add(c3);customers.add(c4);for(Customer c : customers) {sout(c);}class Customer implements Comparable<Customer> {int age;public Customer(int age) {this.age = age;}//比较的规则,paraTo(t.key)。参数k和集合中的每一个k进行比较。//可以指定升序或者降序@Overridepublic int comparaTo(Customer 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; 升序}}

比较器(第二种排序规则)

//创建TreeSet集合的时候,需要使用这个比较器//TreeSet<Customer> customers = new TreeSet<>(); 这样不行,没有通过构造方法传递一个比较器进去TreeSet<Customer> customers = new TreeSet<>(new CusComparator());customers.add(new Custom(1000));customers.add(new Custom(48));customers.add(new Custom(23));customers.add(new Custom(90));//这里省去Custom类的编写。...//单独写一个比较器。比较器实现Comparator接口。class CusComparator implements Comparator<Custom> {@Overridepublic int compare(Custom o1, Custom o2) {return o1.age - o2.age;}}

使用匿名内部类:可以不写比较器。

TreeSet<Customer> customers = new TreeSet<>(new CusComparator<Custom>(){@Overridepublic int compare(Custom o1, Custom o2) {return o1.age - o2.age;}});customers.add(new Custom(1000));...

最终结论:

放到TreeSet或TreeMap集合key部分的元素想做到排序,两种方式实现:

第一种:放在集合中的元素,实现Comparable接口。

第二种:在构造TreeSet或TreeMap集合的时候给它传一个比较器对象。

两种方式的选择:

当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口;如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

自平衡二叉树

TreeSet/TreeMap是自平衡二叉树,遵循左小右大原则存放。

遍历二叉树有三种方式:前序遍历、中序遍历、后序遍历。

TreeSet/TreeMap集合采用中序遍历方式。

Collections集合工具类

集合工具类,方便集合操作。

List<String> list = new ArrayList<>();Collections.synchronizedList(list); //变成线程安全list.add("abf");list.add("tff");list.add("fdsf");Collections.sort(list); //排序

注意:对list集合中元素排序,需要保证list集合中的元素实现了Comparable接口

String类中实现了Comparable接口,可以直接排序。如果是自定义类型,要自己实现。

sort排序只能对List集合排序,不能对Set排序。如果想对Set集合中的元素排序,要将Set集合转换为List集合。

Set<String> set = new HashSet<>();set.add("fkasd");set.add("tret");set.add("gdfg");List<String> myList = new ArrayList<>(set);Collections.sort(myList);

1.4 面试题

HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?

答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 位然后做异或运算。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

当两个对象的 hashCode 相等时会怎么样?

答:会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储。

什么是哈希碰撞,如何解决哈希碰撞?

答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

如果两个键的 hashCode 相同,如何存储键值对?

答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。

2. IO流

2.1 概述

通过IO可以完成硬盘文件的读和写。

IO流的分类:

1. 按照流的方向进行分类,以内存作为参照物。

往内存中去,叫做输入(Input),或者叫做读(Read)。

从内存中出来,叫做输出(Output),或者叫做写(Write)。

2. 按照读取数据方式不同进行分类

按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频等。

按照字符的方式读取数据,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片,声音,视频等文件。只能读取普通文本文件,连word文件都无法读取(word不是纯文本文件)。能用记事本打开的,都是普通文本文件。

学习方法:java中的IO流都已经写好了,我们程序员不需要关心,我们最主要还是掌握,在java中已经提供了哪些流,每个流的特点是什么,每个流对象上的常用方法有哪些。

java中所有的流都是在:java.io.*;下

主要研究:怎么new流对象,调用流对象的哪个方法是读,哪个方法是写。

java IO流的四大家族

java.io.InputStream 字节输入流

java.io.OutputStream 字节输出流

java.io.Reader 字符输入流

java.io.Writer 字符输出流

它们都是抽象类。

所有流都实现了:java.io.Closeable接口,都是可关闭的,都有**close()**方法。

流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会消耗(占用)很多资源。所以,用完流一定要关闭。

所有的输出流都实现了:java.io.Flushable接口,都是可刷新的,都有**flush()**方法。

养成一个好习惯,输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道)。刷新的作用就是清空管道。

注意:如果没有flush()可能会导致丢失数据。

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

2.2 流

文件专属

FileInputStream

文件字节输入流,万能的,可以采用这个流读任何类型的文件。

以字节的方式,完成输入的操作,完成读的操作(硬盘–>内存)

public static void main(String[] args) {FileInputStream fis = null;try {//fis = new FileInputStream("D:\\course\\temp"); // 转义\fis = new FileInputStream("D:/course/temp"); //这两种路径都可以//开始读int readData = 0;while((readData = fis.read()) != -1) {//读不到了,返回-1sout(readData);}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {//在finally语句块中确保流一定关闭if (fis == null) {//避免空指针异常try {fis.close();} catch (IOException e) {e.printStackTrace();}}}}

这个程序的缺点:一次读取一个字节byte,这样内存和硬盘交互太频繁,时间/资源都耗费在交互上了。

int read(byte[] b)一次读取多个字节,提高执行效率。最多读取“数组.length”个字节。

fis = new FileInputStream("demo/src/temp");byte[] bytes = new byte[4]; //准备一个数组,一次读4个字节int readCount = 0;//int read(byte[] b)的返回值是读到字节的数量while((readCount = fis.read(bytes)) != -1) {//把byte数组转换成字符串,读到多少个转换多少个。sout(new String(bytes, 0 , readCount));}

为什么要用String(bytes, 0 , readCount)这个方法?

首先要将读到的字节转换成字符串输出,但是输出不能一次将数组中的值都输出,数组中可能有上一次读取的数据没有被替换掉,所以要用索引截取。

路径的使用:工程Project的根路径是IDEA的默认当前路径。比如上面这个路径,文件temp是放在了demo项目下的src目录下。

常用方法

int available():返回流当中剩余的没有读到的字节数量

fis = new FileInputStream("temp");sout("总字节数量:" + fis.available());int readByte = fis.read(); //读了一个字节sout("剩下多少个字节没有读:" + fis.available()); //假设文件有6个字节,这里返回5

一般用法

byte[] bytes = new byte[fis.available()]; //这种方式不适合太大的文件,因为byte[]数组不能太大int readCount = fis.read(bytes);sout(new String(bytes)); //一次都读出来,不需要循环了

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

fis.skip(3); //跳过3个字节不读sout(fis.read());

FileOutputStream

文件字节输出流,负责写,从内存到硬盘。

FileOutputStream(File file):指定路径中,文件不存在则会新建一个

fos = new FileOutputStream("myfile");String s = "我是一个中国人,哈哈哈!!!";byte[] bs = s.getBytes(); //将字符串转换成byte数组fos.write(bs);fos.flush(); //写完之后,最后一定要刷新

上面写,会先将原文件清空再往里面写,谨慎使用。

FileOutputStream(String name, boolean append):在文件后面追加内容,不会清空原文件。

fos = new FileOutputStream("chapter23/src/tempfile3", true);byte[] bytes = {97,98,99,100};fos.write(bytes);//将byte数组的全部写出,abcdfos.write(bytes, 0, 2); //再写出abfos.flush(); //写完之后,最后一定要刷新

文件复制

使用FileInputStream + FileOutputStream 完成文件的拷贝。

FileInputStream fis = null;FileOutputStream fos = null;try {//创建一个输入流对象fis = new FileInputStream("");//创建一个输出流对象fos = new FileOutputStream("");//最核心,一边读一边写byte[] bytes = new byte[1024*1024]; //一次最多拷贝一兆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,不然其中一个出现异常,可能会影响到另一个流的关闭if (fis == null) {try {fis.close();} catch (IOException e) {e.printStackTrace();}}if (fos != null) {try {fos.close();} catch (IOException e) {e.printStackTrace();}}}

FileReader

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

reader = new FileReader("tempfile"); //创建文件字符输入流char[] chars = new char[4]; //一次读取4个字符int readCount = 0;while((readCount = reader.read(chars)) != -1) {sout(new String(chars, 0, readCount));}

FileWriter

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

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();

转换流

将字节流转换成字符流。也可以转换文件编码。

InputStreamReader

OutputStreamWriter

缓冲流

BufferedReader

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

FileReader reader = new FileReader("Copy02.java");//当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流//外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流//像当前这个程序来说,FileReader就是一个节点流。BufferedReader就是包装流/处理流BufferedReader br = new BufferedReader(reader);String s = null;//readLine,读一个文本行,但不带最后的换行符while((s = br.readLine()) != null) {sout(s);}//关闭流//看源码得知,对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭br.close();

注意:BufferedReader(Reader r) 这个构造方法,只能传字符流,不能传字节流。我们可以通过转换流转换。

//字节流FileInputStream in = new FileInputStream("Copy02.java");//通过转换流 转换InputStreamReader reader = new InputStreamReader(in);BufferedReader br = new BufferedReader(reader);

BufferedWriter

BufferedReader out= new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Copy")));out.write("hello world!");out.write("\n");out.write("hello kitty!");out.flush();out.close();

BufferedInputStreamBufferedOutputStream

数据流

DataOutputStream

数据专属的流。

这个流可以将数据连同数据的类型一并写入文件。

注意:这个文件不是普通的文本文档(用记事本打不开)

//创建数据专属的字节输出流DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));byte b = 100;short s = 200;int i = 300;dos.writeByte(b); //把数据以及数据的类型一并写入到文件当中dos.writeByte(s);dos.writeByte(i);dos.close();

DataInputStream

数据字节输入流。

写的文件,只能使用DataInputStream去读,并且读的时候你需要提前知道写入的顺序。读的顺序需要和写的顺序一致,才可以正常取出数据。

DataInputStream dis= new DataInputStream (new FileInputStream("data"));byte b = dis.readByte();short s = dis.readShort();int i = dis.readInt();sout(b);sout(s);sout(i);dos.close();

标准输出流

PrintWriterPrintStream

标准的字节输出流,默认输出到控制台。

//联合起来写System.out.println("hello world!");//分开写PrintStream ps = System.out;ps.println("hello zhangsan");//标准输出流不用手动close()关闭

可以改变标准字节输出流的方向。

//标准输出流不再指向控制台,指向“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);

对象专属流

ObjectInputStream

ObjectOutputStream

序列化流

序列化:Serialize,java对象存储到文件中。将java对象的状态保存下来的过程。

反序列化:DeSerialize,将硬盘上的数据重新恢复到内存当中,恢复成java对象。

参与序列化和反序列化的对象,必须实现Serializable接口,不然会出异常。

public class Student implements Serialized{private int no;private String name;...}

注意:通过看源码发现,Serializable接口只是一个标志接口,这个接口当中什么代码都没有。它起到标识的作用,java虚拟机看到这个类实现了这个接口,会对这个类进行特殊待遇。

Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口后,会为该类自动生成一个序列化版本号。

建议将序列化版本号手动写出来,不建议自动生成:

private static final long serialVersionUID = 1L;java虚拟机识别

序列化版本号的作用

java虚拟机识别一个类的时候,先通过类名,如果类名一致,再通过序列化版本号。不同的人编写了同一个类,但这两个类确实不是同一个类,这个时候序列化版本就起到作用了。

自动生成序列化版本号的缺陷

一个类,序列化之后,如果再修改代码,会重新编译,这个类的序列化版本号也会发生相应的改变。这时,如果再去反序列化,就会出现异常。

结论

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

序列化

Student s = new Student(1111, "zhangsan");ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));//序列化对象oos.writeObject(s);oos.flush();oos.close();

反序列化

Student s = new Student(1111, "zhangsan");ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));//反序列化,读Object obj = ois.readObject();sout(obj);ois.close();

一次序列化多个对象

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();

反序列化集合

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));List<User> userList = (List<User>)ois.readObject();for(User user : userList){sout(user);}ois.close();

transient关键字,表示游离的,不参与序列化操作!

private int no;private transient String name; //name不参与序列化

IO和Properties联合使用

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

FileReader reader = new FileReader("chapter/userinfo");Properties pro = new Properties();//调用Properties对象的load方法将文件中的数据加载到map集合中pro.load(reader);//通过key,获取valueString username = pro.getProperty("username");String password = pro.getProperty("password");

2.3 File类

和四大家族没有关系,所以File类不能完成文件的读和写。

File对象代表文件和目录路径名的抽象表示形式,一个File对象有可能对应的是目录,有可能是文件。

我们需要掌握File类中常用的方法

exists():判断文件是否存在createNewFile():以文件形式新建mkdir():以目录的形式新建mkdirs():以目录的形式新建多重目录getParent():获取文件的父路径getAbsolutePath():获取绝对路径getName():获取文件名isDirectory():判断是不是一个目录isFile():判断是不是一个文件lastModified():返回文件最后一次的修改时间length():返回文件大小,字节数。

File f1 = new File("D:\\file");sout(f1.exists());if(!f1.exists()) {f1.createNewFile();}if(!f1.exists()) {f1.mkdir();}File f2 = new File("D:/a/b/c/d");if(!f2.exists()) {f2.mkdirs();}

listFiles():获取当前目录下所有的子文件

File f = new File("D:\\course");File[] files = f.listFiles();for(File file : files) {sout(file.getAbsolutePath());sout(file.getName());}

拷贝目录

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);}}}

3. 多线程

3.1 概述

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

对于java程序来说,启动java程序会先启动JVM,JVM就是一个进程。JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的java程序中有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

进程和线程的关系

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

进程A和进程B的内存(资源)独立不共享。

线程A和线程B,堆内存和方法区内存共享,但栈内存独立。一个线程一个栈。

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

多线程并发可以提高效率。

所以,使用了多线程机制,main方法结束,可能程序也不会结束。main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。

分析一个问题,对于单核的CPU来说,可以做到真正的多线程并发吗?

对于4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。但是单核CPU不行。真正的多线程并发,指的是每个线程之间互不影响。

而单核CPU只有一个大脑,不能做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉就是多个事情同时在做。

3.2 实现线程的两种方式

第一种方式

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

创建对象,启动线程。

public static void main(String[] args) {//创建一个分支线程对象MyThread myThread = new MyThread();//启动线程myThread.start();//myThread.run();for(int i = 0; i < 1000; i++) {sout("主线程--->" + i);}}class MyThread extends Thread{@Overridepublic void run() {for(int i = 0; i < 1000; i++) {sout("分支线程--->" + i);}}}

start()方法的作用是:启动一个分支线程,在JVN中开辟一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程启动成功。

启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。

run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

如果不调用start方法开启线程,直接调用run方法,不会分配新的分支栈,这还是单线程。直接调用run方法,相当于还是一个普通的java类。

直接调用run方法的内存图:

调用start方法

第二种方式

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

第二种方式实现接口比较常用,因为第一个类实现了接口,它还可以去继承其他的类,更灵活。

匿名内部类实现:

public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for(int i = 0; i < 100; i++) {sout("分支线程--->" + i);}}});//启动线程t.start();for(int i = 0; i < 100; i++) {sout("主线程--->" + i);}}

3.3 线程的生命周期

新建状态、就绪状态、运行状态、阻塞状态、死亡状态。

3.4 相关方法

获取线程的名字

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

设置对象的名字:线程对象.setName("线程名字");

如果不设置线程对象的名字,默认为:Thread-0、Thread-1 …

获取当前对象

Thread t = Thread.currentThread();返回值t就是当前线程

currentThread方法在哪里出现,当前线程就是哪个线程。

Thread currentThread = Thread.currentThread();sout(currentThread.getName());

让当前线程进入睡眠

static void sleep(long millis)

静态方法:Thread.sleep(1000),参数是毫秒

作用:让当前线程进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用。

注意:不管谁调用的sleep方法,sleep方法在哪个线程,哪个线程睡眠,跟谁调用它没有关系。

如果想唤醒正在睡眠的线程,可以用线程对象.interrupt()方法。

注意:不是中断线程的执行,是终止线程的睡眠。

public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();try{Thread.sleep(1000*5);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt(); //干扰}class MyRunnable implements Runnable {@Overridepublic void run() {sout(Thread.currentThread().getName() + "---> begin");try{Thread.sleep(1000 * 60 * 60 * 24 * 365);} catch(InterruptedException e) {//e.printStackTrace();}sout(Thread.currentThread().getName() + "---> end");}}

中断t线程的睡眠,这种中断睡眠的方式是依靠了java的异常处理机制。就是执行了interrupt()方法,会抛出异常使睡眠结束。

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

终止一个线程

强行终止:线程对象.stop()方法。

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

合理的终止一个线程的执行:使用标记

public static void main(String[] args) {MyRunable r = new MyRunable();Thread t = new Thread(r);t.setName("t");t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {}//5秒后,终止这个线程r.run = false; //你想什么时候终止t的执行,将标记改成false,就结束了}class MyRunable implements Runable {boolean run = true;@Overridepublic void run() {for(int i = 0; i < 10; i++) {if(run) {...} else {//可以在return前,保存数据return;}}}}

3.5 线程调度

常见的线程调度

抢占式调度模型

哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。

java采用的就是抢占式调度模型。均分式调度模型

平均分配CPU时间片。每个线程占有的CPU时间片长度一样。

平均分配,一切平等。

有一些编程语言,线程调度模型采用的是这种方式。线程调度相关方法实例方法

void setPriority(int newPriority):设置线程的优先级。

int getPriority():获取线程优先级

线程优先级最低是1(MIN_PRIORITY),最高是10(MAX_PRIORITY),默认是5(NORM_PRIORITY)。

优先级比较高的获取CPU时间片可能会多一些。(大概率多一些)

void join():合并线程

t.join(); //t合并到当前线程中,当前线程受阻塞,t线程执行完,再执行当前线程

静态方法

static void yield():让位

暂停当前正在执行的线程对象,并执行其他线程。

yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

3.6 线程安全(重点)

关于多线程并发环境下,数据的安全问题。

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

在多线程并发的环境下,满足3个条件之后,会存在线程安全问题

条件1:多线程并发。

条件2:有共享数据

条件3:共享数据有修改的行为。

注意,java中有三大变量:实例变量、静态变量、局部变量。

实例变量在堆中,

静态变量在方法区,

局部变量在栈中。

以上三个变量,局部变量永远不会存在线程安全问题,因为局部变量不共享数据(一个线程一个栈)。堆和方法区只有1个,都是多线程共享的,所以存在线程安全问题。

解决线程安全问题

用排队执行解决线程安全问题。这种机制被称为:线程同步。实际上就是线程不能并发了,线程必须排队执行。

线程同步就是线程排队了,线程排队了就会牺牲一部分效率。数据安全第一位,数据安全了,再去谈效率。

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

Account类

private String actno; //账号private double balance; //余额//省略构造方法,省略getter和setter方法...//取款方法public void withdraw(double money) {double before = this.getBalance(); //取款前的余额double after = before - money; //取款后的余额//如果线程t1执行到这,但还没执行下面这行代码,此时t2线程也进入withdraw方法,就出问题了。//模拟网络延迟,一定会出问题try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after); //更新余额}

AccountThread

public class AccountThread extends Thread {//两个线程必须共享同一个账户对象private Account act;public AccountThread(Account act) {this.act = act;}public void run() {double money = 5000;act.withdraw(money);sout(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功,余额" + act.getBalance());}}

主方法,创建两个对象

public class Test{public static void main(String[] args) {//创建一个账户对象Account act = new Account("act-001", 10000);//创建两个线程Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);t1.setName("t1");t2.setName("t2");t1.start();t2.start();}}

结果:

t1对act-001取款5000.0成功,余额5000.0

t2对act-001取款5000.0成功,余额5000.0

同步代码块synchronized

将上面的例子修改成线程安全的,加synchronized同步代码块。

以下几行代码必须是线程排队的,不能并发。一个线程执行结束,另一个线程才能进来。

public void withdraw(double money) {synchronized (this){double before = this.getBalance();double after = before - money;try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}}

这里至关重要的是,synchronized括号中传递的数据,这个数据必须是多线程共享的数据,才能达到多线程排队。

括号中写什么,是要看你想让哪些线程同步。

假设t1、t2、t3、t4、t5,有5个线程,你想让t1、t2、t3排队,那括号中要写t1、t2、t3共享的对象,而这个对象对于t4、t5来说不是共享的。

在java语言中,任何一个对象都有“一把锁”,这把锁其实是一个标记。

同步代码块执行原理

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

进入锁池,可以理解成一种阻塞状态。

对共享数据的理解

this指的是对象本身,这个程序中是指Account对象。

synchronized (this){...}

创建的两个线程,使用的同一个对象,那么就算是共享数据。

Account act = new Account("act-001", 10000);Thread t1 = new AccountThread(act);Thread t2 = new AccountThread(act);Account act2 = new Account("act-002", 10000);Thread t3 = new AccountThread(act2);

而下面又创建了一个Account对象,这个对象创建出来的线程,跟上面数据是不共享的,这个Account对象的this是另一个。

总之,想清楚synchronized括号中的对象,是不是想要共享的。

写成abc字符串,创建出来的线程就都是同步的了。

synchronized ("abc"){...}

在实例方法上使用synchronized

synchronized出现在实例方法上,一定锁的是this(共享对象是this),不能是其它对象了。

缺点:不灵活;整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。

优点:代码节俭。

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

public synchronized void withdraw(double money) {double before = this.getBalance();double after = before - money;try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}this.setBalance(after);}

synchronized的第三种写法

在静态方法上使用synchronized,表示类锁类锁永远只有1把

对象锁:1个对象1把锁,100个对象100把锁。

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

public static void main(String[] args) {MyClass mc1 = new MyClass();MyClass mc2 = new MyClass();Thread t1 = new MyThread(mc1);Thread t2 = new MyThread(mc2);...}class MyClass {public synchronized static void doSome() {sout("doSome begin");try{Thread.sleep(1000*10);} catch (InterruptedException e) {e.printStackTrace();}sout("doSome over");}public synchronized static void doOther() {sout("doOther begin");sout("doOther end");}}

t2需要等待t1执行完才执行,因为类锁永远只有1把,即使创建了两个对象,用的也是同一个锁。

死锁现象

死锁很难调试,不会出异常也不报错。要求会写死锁代码,只有回写了,才会在以后的开发中注意这个事儿。

public class DeadLock {public static void main(String[] args) {Object o1 = new Object();Object o2= new Object();Thread t1 = new MyThread(o1,o2);Thread t2 = new MyThread(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个线程1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。

3.7 守护线程和定时器

java语言中线程分为两大类:

用户线程

守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点:

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

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

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

实现:在启动线程之前,将线程设置为守护线程。

public static void main(String[] args) {Thread t = new BakDataThread();t.setName("备份数据的线程");t.setDaemon(true); //将主线程设置为守护线程t.start();...}

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

如:每周要进行银行账户的总账操作;每天要进行数据的备份操作。

在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的,在java中其实可以采用多种方式实现:

sleep方法,设置睡眠时间。这种方式是最原始的定时器。(不好)java.util.Timer,java类库中的定时器。Spring提供的SpringTask框架。目前开发中使用较多。

实现定时器,用Timer对象中的schedule方法

public static void main(String[] args) {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("-03-09 14:50:00");timer.schedule(new LogTimerTask(), firstTime, 1000*10);}class LogTimerTask extends TimerTask {@Overridepublic void run() {//编写任务}}

3.8 实现线程的第三种方式

实现Callable接口(JDK8新特性)。

这种方式实现的线程可以获取线程的返回值。之前讲解的两种方式无法获取线程返回值,因为run方法返回void。

思考:系统委派一个线程去执行一个任务,该线程执行完之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?

使用第三种方式:实现Callable接口。

public static void main(String[] args) throws Exception{//创建一个“未来任务类”对象//参数非常重要,需要给一个Callable接口实现类对象FutureTask task = new FutureTask(new Callable() {@Overridepublic Object call() throws Exception {//call方法相当于run方法,只不过call方法有返回值//线程执行一个任务,执行之后可能会有一个执行结果sout("call method begin");Thread.sleep(1000*10);sout("call method end");int a = 100;int b = 100;return a+b;}});Thread t = new Thread(task);t.start();//用get方法获取t线程的返回结果Object obj = task.get(); //get方法执行会导致“当前线程阻塞”//main方法这里的程序要想执行,必须等待get()方法的结束//而get方法可能需要很久,因为get方法是为了拿另一个线程的执行结果,需要时间sout("线程执行结果:" + obj);sout("hello world!");}

优点:可以获取到线程的执行结果。

缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

3.9 生产者消费者模型

wait和notify方法

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

wait方法表示:让正在对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。

notify方法表示:对象调用wait方法进入等待状态,直到调用notify方法才唤醒。

生产者和消费者模型是为了专门解决某个特定需求的。

wait方法会让正在对象上活动的当前线程进入等待状态,并且释放之前对象占有的锁。

notify方法只会通知,不会释放占有的锁。

实现生产者消费者模型:

仓库采用list集合,假设list集合中只能存储1个元素,1个元素就表示仓库满了。

如果list集合中元素个数是0,就表示仓库空了。

保证list集合中永远都是最多存储1个元素。

做到:生产1个,消费一个。

public static void main(String[] args) {List list1 = 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 Produces(List list) {this.list = list;}@Overridepublic void run() {while(true) {//给仓库对象list加锁synchronized(list) {if(list.size() > 0) {try{//线程进入等待状态,并释放Producer占有的锁list.wait();} catch (InterruptedException e) {e.printStackTrace();}}//程序能执行到这里说明仓库是空的,可以生产Object obj = new Object();list.add(obj);sout(Thread.currentThread().getName() + "--->" + obj);list.notify();}}}}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.wait();} catch (InterruptedException e) {e.printStackTrace();}}//程序能执行到这里说明仓库有数据,进行消费Object obj = list.remove(0);sout(Thread.currentThread().getName() + "--->" + obj);//唤醒生产者生产list.notify();}}}}

4. 遗漏的基础

4.1 static关键字

变量分类:

局部变量:在方法体当中声明的变量。

成员变量:在方法体外声明的变量。

成员变量又可分为:实例变量、静态变量

通过static关键字修饰的,都是类相关的,访问时用类名.的方式访问。不需要对象参与即可访问。

没有通过static关键字修饰的,是对象相关的,访问时用对象.的方式访问,需要先new对象。

静态变量在类加载时初始化,不需要new对象,静态变量的空间就开出来了。

静态变量存储在方法区。

静态变量如果没有赋值,默认为null。

什么时候使用static修饰?

1.一个属性,每次实例化都是相同的值,不建议定义为实例变量,浪费内存空间。建议定义成类级别的,用static修饰,在方法区中只保留一份,节省内存开销。

2.当属于同一个类的所有对象出现共享数据时,需要将存储这个共享数据的成员变量用static修饰。

注意:实例的,一定要用引用.来访问;静态的可以用类名.也可以用对象.来访问。

静态的,使用对象.访问,会转换成类名.去访问。不建议使用对象.访问,因为会给其他程序员造成困惑。

Chinese c1 = null;sout(c1.country); //如果country是静态变量,可以访问。因为c1会被转换成类名。//如果是country是实例变量,会出现空指针异常。

通过以上结论,我们可以得出,“空引用”访问“实例”相关的,都会出现空指针异常。

静态代码块

static静态代码块:类加载时执行,并且只执行一次。

static {sout("A");}static {sout("B");}public static void main(String[] args) {sout("Hello World!");}static {sout("C");}

结果:

A

B

C

Hello World!

静态代码块在类加载时执行,并且在main方法执行之前执行。一般按照自上而下的顺序执行。

静态方法不存在方法覆盖

方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。

方法覆盖需要和多态机制联合起来使用才有意义。

Animal a = new Cat(); //多态a.doSome(); //doSome是静态方法,这句代码会被转换成Animal.doSome(),跟子类无关了

4.2 多态

多态概述

多种形态,多种状态,编译和运行有两个不同的状态。

编译器叫做静态绑定。

运行期叫做动态绑定。

Animal a = new Cat();a.move();

编译的时候,编译器发现a的类型是Animal,所以编译器会去Animal类中找move()方法。找到了,绑定,编译通过。但是运行的时候和底层堆内存当中的实际对象有关。真正执行的时候会自动调用“堆内存中真实对象”的相关方法。

向上转型和向下转型的概念

向上转型:子—>父,又被称为自动类型的转换:Animal a = new Cat();

向下转型:父—>子,又被称为强制类型转换:Cat c = (Cat) a;

需要调用或执行子类对象中特有的方法,必须进行向下转型,才可以调用

风险:容易出现ClassCastException(类型转换异常)。

所以,要使用instanceof运算符,在程序运行阶段动态的判断某个引用指向的对象是否为某一种类型。

不管是向上转型还是向下转型,首先他们之间必须有继承关系,这样编译器就不会报错。

多态的实际应用

public class Pet {//吃的行为,这个方法可以不给具体的实现public void eat() {}}public class Cat extends Pet {public void eat() {sout("猫吃鱼...");}}public class Master {//编译的时候,编译器发现pet是Pet类,会去Pet类中找eat()方法,找到了,编译通过//运行时,调用实际对象对应的eat方法public void feed(Pet pet) {pet.eat();}}public static void main(String[] args) {Master zhangsan = new Master();Cat a = new Cat();zhangsan.feed(a); //这里传递的a就是使用了多态,因为Master类中feed方法的参数是Pet对象,Pet pet = new Cat()}

分析:如果以后有新的需求,只需要添加一个新的类,然后再去测试就行,不用修改其他类中的代码。

在实际开发中,用户的需求增加了,也就只用增加新的功能,不用修改原来的代码。

软件在扩展新需求过程当中,修改的越少越好。修改的越多,你的系统当前的稳定性就越差,未知的风险就越多。

这里涉及到一个软件的开发原则:OCP(开闭原则)

软件开发有七大原则,这是最基本的一条。

开闭原则:对扩展开放,对修改关闭

4.3 抽象类和接口

抽象类

类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。

抽象类无法实例化,无法创建对象,所以抽象类是用来被子类继承的。

抽象类也属于引用数据类型。

abstract和final不能联合使用,这两个关键字是对立的。

抽象类的子类可以是抽象类。

抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法是供子类使用的。

抽象方法:抽象方法表示没有实现的方法,没有方法体的方法。

抽象类不一定有抽象方法,抽象方法必须出现在抽象类中。

一个非抽象类继承抽象类,必须将抽象类中的方法实现了。

接口

接口是一种“引用数据类型”。

接口是完全抽象的。

接口支持多继承。

接口中只有常量和抽象方法。

接口中所有的元素都是public修饰的。

接口中抽象方法的public abstract可以省略。

接口中常量的public static final可以省略。

接口中方法不能有方法体。

一个非抽象类,实现接口的时候,必须将接口中所有方法加以实现。

一个类可以实现多个接口。

extends和implement可以共存,extends在前,implement在后。

使用接口,写代码的时候,可以使用多态。

抽象类和接口的区别:

抽象类是半抽象的;接口是完全抽象的。

抽象类没有构造方法;接口中没有构造方法。

接口和接口之间支持多继承;类和类之间只能单继承。

一个类可以同时实现多个接口;一个类只能继承一个抽象类。

接口中只允许出现常量和抽象方法。

接口一般都是对“行为”的抽象。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。