Java基础知识(二)
参考文档:https://docs.oracle.com/en/java/javase/17/docs/api/index.html
一、集合
1、 Collection
List:存和取顺序是一致的,可重复,有索引
Set:存和取的顺序是不一定一致的,不可重复,无索引
总结:
- 如果想要集合中的元素可重复 —— 用ArrayList集合,基于数组
- 如果想要集合中的元素可重复且当前的增删操作多于查询 —— 用LinkedList集合,基于链表
- 如果想要对集合中的元素去重 —— 用HashSet集合,基于哈希表
- 如果想要对集合中的元素去重且保证存取顺序 —— 用LinkedHashSet集合,基于哈希表和双链表
- 如果想对集合中的元素进行排序 —— 用TreeSet集合,基于红黑树
方法名称 | 说明 |
---|---|
public boolean add(E e) | 把给定的对象添加到当前集合中 |
public void clear() | 清空集合中的所有元素 |
public boolean remove(E e) | 把给定的对象在当前集合中删除 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空 |
public int size() | 返回集合中元素的个数/集合的长度 |
全部单列集合都可以继承使用
添加数据:
public boolean add(E e)
- 往List系列集合添加数据,永远返回true,因为List允许元素重复
- 往Set系列集合添加数据,如果当前要添加的元素不存在,返回true,否则为false,因为Set不允许重复
删除数据:
public boolean remove(E e)
- 因为Collection里面定义的是共性的方法,所以不可以通过索引删除,只能通过元素对象删除
- 删除元素存在,删除成功返回true,否则失败返回false
包含元素:
public boolean contains(Object obj)
- 底层是依赖equals方法判断对象是否一致
- 如果判断是否包含自定义对象,需要重写equals方法,否则默认使用object类中的equals依赖地址值进行判断
// 如果判断属性值相同则对象一致的话 需要重写equals方法 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); }
遍历方式:
迭代器遍历
遍历结束,指针不会复位
循环中只能使用一次next方法
遍历时,不可以用collection的方法进行添加或者删除 否则报错ConcurrentModificationException
使用迭代器提供的方法进行删除(it.remove())
package TestCode; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class TestClass { public static void main(String[] args) { Collection<String> collection = new ArrayList(); collection.add("abc"); collection.add("edf"); Iterator<String> iterator = collection.iterator(); while(iterator.hasNext()){ String str = iterator.next(); System.out.println(str); } } }
Collection获取迭代器 Iterator iterator() 返回迭代器对象,默认指向当前集合的0索引 Iterator中常用方法 boolean hasNext() 判断当前位置是否有元素,有返回true,否则false E next() 获取当前位置的元素,并将迭代器对象移向下一个位置
增强for遍历
底层是一个迭代器
范围:所有单列集合和数组
package TestCode; import java.util.ArrayList; import java.util.Collection; public class TestClass { public static void main(String[] args) { Collection<String> collection = new ArrayList(); collection.add("abc"); collection.add("edf"); for (String s : collection) { //s就是一个第三方变量,循环过程中表示集合中的每一个元素,不会改变集合中原本的数据 System.out.println(s); } } }
Lambda表达式遍历
- 利用匿名内部类的形式
package TestCode; import java.util.ArrayList; import java.util.Collection; import java.util.function.Consumer; public class TestClass { public static void main(String[] args) { Collection<String> collection = new ArrayList(); collection.add("abc"); collection.add("edf"); /*collection.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } });*/ collection.forEach(s -> System.out.println(s)); } }
1.1、List
List特有的方法
方法名称 说明 void add(int index, E element) 在此集合中的指定位置插入指定元素 E remove(int index) 删除指定索引处的元素,返回被删除的元素 E set(int index, E element) 修改指定索引处的元素,返回被修改的元素 E get(int index) 返回指定索引处的元素 遍历:
- 迭代器
- 列表迭代器
- 增强for
- Lambda表达式
- 普通for循环
package TestCode; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ListIterator; public class TestClass { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); // 1. 迭代器 -- 遍历过程中删除元素 Iterator<String> it = list.iterator(); while (it.hasNext()) { String str = it.next(); System.out.println(str); if ("bbb".equals(str)) { it.remove(); } } System.out.println(list); // 2.增强for -- 仅仅遍历 for (String s : list) { System.out.println(s); } // 3.Lambda -- 仅仅遍历 list.forEach(s -> System.out.println(s)); //4. 普通for -- 遍历过程中操作索引 for (int i = 0; i < list.size(); i++) { String str = list.get(i); System.out.println(str); } //5. 列表迭代器 -- 遍历过程中添加元素 ListIterator<String> it = list.listIterator(); while(it.hasNext()) { String str = it.next(); if("bbb".equals(str)) { it.add("qqq"); } } System.out.println(list); } }
1.1.1 ArrayList
底层原理:
利用空参创建的集合,在底层创建一个默认长度为0的数组
添加第一个元素时,底层会创建一个新的长度为10的数组名字为elementData
还有一个成员变量size记录元素的个数和下次存入的位置
存满时,会扩容1.5倍
如果一次添加多个元素,1.5倍放不下则新创建数组的长度以实际为准
1.1.2 LinkedList
底层数据结构是双链表,查询慢,首尾操作速度快
特有方法 说明 public void addFirst(E e) 在该列表开头插入指定元素 public void addLast(E e) 将指定元素追加到此列表的末尾 public E getFirst() 返回此列表的第一个元素 public E getLast() 返回此列表的最后一个元素 public E removeFirst() 从此列表中删除并返回第一个元素 public E removeLast() 从此列表中删除并返回最后一个元素
1.1.3 迭代器
1.1.4 泛型
格式:<数据类型>
泛型只能支持引用数据类型,如果不写默认是Object类型
指定泛型的具体类型之后,传递数据时,可以传入该类类型或其子类类型
泛型的擦除:在编译的时候会检查添加的数据是不是和泛型里的数据类型一致,如果不是编译报错
Java文件
ArrayList<String> list = new ArrayList<>();
编译变成:
class字节码文件
ArrayList list = new ArrayList();
泛型类
当一个类中,某个变量的数据类型不确定,就可以定义带有泛型的类,类里所有方法都能用
创建该类对象时,E就确定了类型
public class ArrayList<E> { }
package TestCode; import java.util.Arrays; public class MyArrayList <E>{ Object[] obj = new Object[10]; int size; public boolean add(E e){ obj[size] = e; size++; return true; } public E get(int index){ return (E)obj[index]; } public String toString(){ return Arrays.toString(obj); } }
泛型方法
方法中形参类型不确定,在方法申明上定义自己的类型,只能在本方法使用
public<T> void show(T t){ }
package TestCode; import java.util.ArrayList; public class MyArrayList { private MyArrayList(){}; public static <E> void addAll(ArrayList<E> list, E e1, E e2, E e3){ list.add(e1); list.add(e2); list.add(e3); } // E...e 是可变参数 public static <E> void addAll2(ArrayList<E> list, E...e){ for (E element : e) { list.add(element); } } }
package TestCode; import java.util.ArrayList; public class TestClass { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); MyArrayList.addAll(list, 111, 222, 333); System.out.println(list); ArrayList<String> list1 = new ArrayList<>(); MyArrayList.addAll2(list1, "abc", "def"); System.out.println(list1); } }
泛型接口
public interface List<E>{ }
实现类给出具体类型
public class MyArrayList implements List<String>{...} //------------------------------------------------------- MyArrayList myArrayList = new MyArrayList(); myArrayList.add("abc");
实现类延续泛型,创建对象的时候确定类型
public class MyArrayList<E> implements List<E>{...} //------------------------------------------------------- MyArrayList<Integer> myArrayList = new MyArrayList<>(); myArrayList.add(123);
泛型继承和通配符
- 泛型不具备继承性,但数据具备继承性
- 泛型里面写的是什么类型,就只能传递什么类型的数据
package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { //泛型不具备继承性 ArrayList<Ye> list1 = new ArrayList<>(); ArrayList<Fu> list2 = new ArrayList<>(); ArrayList<Zi> list3 = new ArrayList<>(); method(list1); // method(list2); -- 报错 // method(list3); -- 报错 //数据具备继承性 list1.add(new Ye()); list1.add(new Fu()); list1.add(new Zi()); } public static void method(ArrayList<Ye> list) { } } class Ye { } class Fu extends Ye { } class Zi extends Fu { } class Stu { }
- 泛型通配符 ? 表示不确定的类型,但可以进行类型限定
- ? extends E: 表示可以传递E或者E所有子类类型
- ? super E: 表示可以传递E或者E所有父类类型
package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { ArrayList<Ye> list1 = new ArrayList<>(); ArrayList<Fu> list2 = new ArrayList<>(); ArrayList<Zi> list3 = new ArrayList<>(); ArrayList<Stu> list4 = new ArrayList<>(); method(list1); method(list2); method(list3); //method(list4); -- 报错 method2(list1); method2(list2); //method2(list3); -- 报错 //method2(list4); -- 报错 } public static void method(ArrayList<? extends Ye> list) { } public static void method2(ArrayList<? super Fu> list) { } } class Ye { } class Fu extends Ye { } class Zi extends Fu { } class Stu { }
1.2、Set
set系列集合
无序: 存取顺序不一致
不重复: 可以去除重复
无索引: 没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素set集合的实现类
HashSet: 无序、不重复、无索引 —— 数据去重默认使用
LinkedHashSet: 有序、不重复、无索引——数据去重且存取有序时使用
TreeSet: 可排序、不重复、无索引
1.2.1 HashSet
底层原理:
- 采取哈希表存储数据
- 哈希表组成:JDK8之前 数组+链表;JDK8开始 数组+链表+红黑树
- 哈希值:是int类型 (哈希碰撞:不同属性或地址值计算出来的哈希值一样 eg:“abc"和"acD”)
@Override public int hashCode() { return Objects.hash(name, age); }
创建一个默认长度16,默认加载因为0.75的数组,数组名table
根据元素的哈希值跟数组的长度计算出应存入的位置
int index = (数组长度 - 1) & 哈希值;
判断当前位置是否为null
如果是null直接存入
如果位置不为null,表示有元素,则调用equals方法比较属性值
- JDK8以前 一样: 不存; 不一样: 存入数组,形成链表——新元素存入数组,老元素挂在新元素下
- JDK8以后 一样: 不存 ; 不一样: 存入数组,形成链表——新元素直接挂在老元素下面
加载因子是HashSet扩容时机,当(16*0.75=12)存了12个元素时,扩容两倍即32
JDK8以后,当链表长度大于8且数组长度大于等于64时,当前链表自动转成红黑树
如果集合中存储的时自定义对象,必须重写hashCode和equals方法,因为HashSet是利用hashCode和equals机制保证数据去重的
1.2.2 LinkedHashSet
原理: 底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。
1.2.3 TreeSet
TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
TreeSet集合默认的规则
- 对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序
- 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序。
TreeSet两种比较方式
- 默认排序或自然排序 - 实现Comparable接口指定比较规则
- 比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则
/*方法一 ;默认排序或自然排序 - 实现Comparable接口指定比较规则*/ package TestCode; public class Student implements Comparable<Student>{ private String name; private int age; public Student() { } public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { //this: 表示当前要添加的元素 // o: 表示已经在红黑树存在的元素 System.out.println("this: " + this); System.out.println("o: " + o); //返回值: //负数: 表示当前要添加的元素是小的,存左边 //正数: 表示当前要添加的元素是大的,存右边 //0 :表示当前要添加的元素已经存在,舍弃 return this.getAge() - o.getAge(); } }
/*方法二:比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则*/ package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { //o1:表示当前要添加的元素 //o2:表示已经在红黑树上的元素 TreeSet<String> ts = new TreeSet<>((o1, o2) -> { //长度排序 int i = o1.length() - o2.length(); //长度相同按字母排序 i = i == 0 ? o1.compareTo(o2) : i; return i; }); } }
2、Map
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值可以重复
- 键和值是一一对应的,每一个键只能找到自己对应的值
- 键+值这个整体我们称之为“键值对”或者“键值对对象”,在Java中叫做“Entry对象
方法名称 说明 V put(K key, V value) 添加/覆盖元素 V remove(Object key) 根据键删除键值对元素 void clear() 移除所有键值对元素 boolean containsKey(Object key) 判断集合是否包含指定的键 boolean containsKey(Object value) 判断集合是否包含指定的值 boolean isEmpty() 判断集合是否为空 int size() 集合的长度/集合键值对的个数 put: 添加/覆盖
- 如果键不存在,直接把键值对添加到集合中并返回null
- 如果键存在,会把原有键值对覆盖并返回被覆盖的值
遍历
- 键找值
- 键值对
- Lambda表达式
/*键找值*/ package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { Map<String,String> map = new HashMap<>(); map.put("zhangsan","zs"); map.put("lisi","ls"); map.put("wangwu","wu"); //获取所有的键并存放到单列集合 Set<String> keys = map.keySet(); //遍历集合获得每一个键 for (String key : keys) { //利用键获得值 String value = map.get(key); System.out.println(key + " = " + value); } } }
/*键值对*/ package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { Map<String,String> map = new HashMap<>(); map.put("zhangsan","zs"); map.put("lisi","ls"); map.put("wangwu","wu"); //获取所有的键值对对象并存放到单列集合,Entry是Map的内部接口 Set<Map.Entry<String, String>> entries = map.entrySet(); //遍历集合获得每一个键值对对象 for (Map.Entry<String, String> entry : entries) { System.out.println(entry.getKey() + " = " + entry.getValue()); } } }
/*Lambda*/ package TestCode; import java.util.*; import java.util.function.BiConsumer; public class TestClass { public static void main(String[] args) { Map<String,String> map = new HashMap<>(); map.put("zhangsan","zs"); map.put("lisi","ls"); map.put("wangwu","wu"); map.forEach((key, value) -> System.out.println(key + " = " + value)); } }
2.1、HashMap
HashMap底层是哈希表结构的
依赖hashCode方法和equals方法保证键的唯一
如果键存储的是自定义对象需要重写hashCode和equals方法
如果值存储的是自定义对象不需要重写hashCode和equals方法
底层原理和HashSet一样,但是利用键计算哈希值
package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { // 投票随机生成 String[] arr = {"A","B","C","D"}; ArrayList<String> list = new ArrayList<>(); Random r = new Random(); for (int i = 0; i < 80; i++) { int index = r.nextInt(arr.length); list.add(arr[index]); } // 统计投票结果 HashMap<String,Integer> map = new HashMap<>(); for (String s : list) { if (map.containsKey(s)) { int count = map.get(s); count++; map.put(s,count); } else{ map.put(s,1); } } // 获取最大值 System.out.println(map); int max = 0; Set<Map.Entry<String, Integer>> entries = map.entrySet(); for (Map.Entry<String, Integer> entry : entries) { int count = entry.getValue(); max= count>max?count:max; } System.out.println("max = " + max); // 将符合最大值的键打印出来 for (Map.Entry<String, Integer> entry : entries) { int count = entry.getValue(); if(count==max) { System.out.println(entry.getKey()); } } } }
/* 源代码解析 */ //The default initial capacity static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //The load factor static final float DEFAULT_LOAD_FACTOR = 0.75f; //数组 Node<K,V>[] table; /* HashMap每个对象包含的内容: 1.1 链表中的键值对对象 final int hash; //键的哈希值 final K key; //键 V value; //值 Node<K,V> next; //下一个节点的地址值 1.2红黑树中的键值对对象 final int hash; //键的哈希值 final K key; //键 V value; //值 TreeNode<K,V> parent; // 父节点地址值 TreeNode<K,V> left; //左子节点地址值 TreeNode<K,V> right; //右子节点地址值 boolean red; //节点颜色 */ // 添加元素 HashMap<String, Integer> hm = new HashMap<>(); hm.put("aaa", 111); hm.put("bbb", 222); /* 添加元素的三种情况: 1.数组位置为null 2.数组位置不为null,键重复,元素覆盖 3.数组位置不为null,键不重复,挂在下面形成链表或红黑树 */ /* 参数一:键 参数二:值 返回值:被覆盖元素的值,如果没有被覆盖返回null */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /* 利用键计算出对应的哈希值,并对其进行处理 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } /* 参数一:键的哈希值 参数二:键 参数三:值 参数四:如果键重复是否保留 - true 老元素的值保留,不被覆盖 - false 老元素的值不保留,进行覆盖 (旧的键值对还在,覆盖的只是旧的键值对的数据) */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { //定义局部变量,用来记录哈希表中数组的地址值 Node<K,V>[] tab; //记录键值对地址值 Node<K,V> p; //当前数组的长度 int n; //索引 int i; //把哈希表中数组的地址值,赋值给局部变量tab tab = table; if (tab == null || (n = tab.length) == 0) /*resize() 1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组 2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容条件 - 没有,不做任何操作 - 有, 会把数组扩容到原先的两倍,并将数据全部转移到新哈希表中 */ tab = resize() //把当前数组长度赋值给n n = tab.length; //用数组的长度和哈希值进行计算,获取当前键值对对象在数组中应存放的位置 i = (n - 1) & hash; //获取数组中对应元素的数据 p = tab[i]; if (p == null) //创建一个键值对对象,放到数组中 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //p.hash 数组中键值对的哈希值 //hash 要添加的键值对的哈希值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //判断数组中获取出来的键值对是不是红黑树节点 //按照红黑树规则添加到红黑树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //数组中获取出来的键值对是存在链表中的 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //创建一个新的节点挂在下面形成链表 p.next = newNode(hash, key, value, null); //判断长度是否超过8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //判断数组是否大于等于64 //同时符合则转换成红黑树 treeifyBin(tab, hash); break; } //如果哈希值一样,会调用equals()方法比较内部属性值是否相同,防止哈希碰撞 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果e为null 不需要覆盖元素 //如果e不为null 需要覆盖元素 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //value是当前应添加的值 e.value = value; afterNodeAccess(e); //返回旧数据 return oldValue; } } ++modCount; // threshold = 数组长度 * 0.75 哈希表扩容的时机 if (++size > threshold) resize(); afterNodeInsertion(evict); // 当前没有覆盖任何元素则返回null return null; }
2.2、LinkedHashMap
原理: 底层数据结构是依然哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录存储的顺序
2.3、TreeMap
TreeMap跟TreeSet底层原理一样,都是红黑树结构的。
由键决定特性: 不重复、无索引、可排序
可排序: 对键进行排序(注意: 默认按照键的从小到大进行排序,也可以自己规定键的排序规则)
代码书写两种排序规则
实现Comparable接口,指定比较规则
创建集合时传递Comparator比较器对象,指定比较规则。
package TestCode; import java.util.TreeMap; public class TestClass { public static void main(String[] args) { String str = "aababcabcdabcde"; TreeMap<Character, Integer> tm = new TreeMap<>(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (tm.containsKey(c)) { int count = tm.get(c); count++; tm.put(c,count); }else { tm.put(c,1); } } System.out.println(tm); } }
/* 源代码解析 */ /*TreeMap每一个节点的内部属性*/ K key; //键 V value; //值 Entry<K,V> left; //左子节点 Entry<K,V> right; //右子节点 Entry<K,V> parent; //父节点 boolean color = BLACK; //节点颜色 /*成员变量*/ private final Comparator<? super K> comparator;//比较器对象 private transient Entry<K,V> root;//根节点 private transient int size = 0;//集合的长度、节点个数 /*空参构造*/ public TreeMap() { comparator = null; } /*带参构造*/ public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } /*添加元素*/ public V put(K key, V value) { return put(key, value, true); } /* 参数一:键 参数二:值 参数三:当键重复的时候,是否需要覆盖 true 覆盖 false 不覆盖 */ private V put(K key, V value, boolean replaceOld) { //获取根节点的地址值,赋值给局部变量 Entry<K,V> t = root; /* 判断根节点是否为null - 如果为null,表示当前是第一次添加,会把当前添加的元素当作根节点 - 如果不为null,表示当前不是第一次添加,跳过这个判断执行下面代码 */ if (t == null) { //创建一个Entry对象,并当作根节点 addEntryToEmptyMap(key, value); //返回null表示没有覆盖任何元素 return null; } //表示两个元素的键比较之后的结果 int cmp; //表示当前要添加节点的父节点 Entry<K,V> parent; /* 表示当前的比较规则 - 默认的自然排序,comparator是null,cpr也是null - 比较器排序方式,comparator记录的是比较器 */ Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else { V oldValue = t.value; if (replaceOld || oldValue == null) { t.value = value; } return oldValue; } } while (t != null); } else { Objects.requireNonNull(key); @SuppressWarnings("unchecked") //将键进行强转成Comparable类型 //键必须实现Comparable接口,如果没有实现则会报错 Comparable<? super K> k = (Comparable<? super K>) key; do { //把根节点当作当前节点的父节点 parent = t; //调用compareTo方法比较根节点和当前要添加节点的大小关系 cmp = k.compareTo(t.key); if (cmp < 0) //如果比较的结果是负数,到根节点的左边去找 t = t.left; else if (cmp > 0) //如果比较的结果是正数,到根节点的右边去找 t = t.right; else { //如果比较结果为0,则会覆盖 V oldValue = t.value; if (replaceOld || oldValue == null) { t.value = value; } return oldValue; } } while (t != null); } //把当前节点按照指定规则进行添加 addEntry(key, value, parent, cmp < 0); return null; } private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) { Entry<K,V> e = new Entry<>(key, value, parent); if (addToLeft) parent.left = e; else parent.right = e; //添加完毕之后,需要按照红黑树的规则进行调整 fixAfterInsertion(e); size++; modCount++; } private void fixAfterInsertion(Entry<K,V> x) { //红黑树节点默认是红色 x.color = RED; /* 按照红黑树规则进行调整 parentOf 获取x的父节点 parentOf(parentOf(x)) 获取x的爷爷节点 leftOf:左子节点 */ while (x != null && x != root && x.parent.color == RED) { //判断当前节点的父节点是爷爷节点的左子节点还是右子节点 //目的:为了获取当前节点的叔叔节点 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //表示当前节点的父节点是爷爷节点的左子节点 //就可以用rightOf获取当前节点的叔叔节点 Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { //叔叔节点为红的 setColor(parentOf(x), BLACK);//将父节点设为黑色 setColor(y, BLACK);//将叔叔节点设为黑色 setColor(parentOf(parentOf(x)), RED);//将爷爷节点设为红色 x = parentOf(parentOf(x));//把爷爷节点设置为当前节点 } else { //叔叔节点为黑色 //判断当前节点是否为父节点的右子节点 if (x == rightOf(parentOf(x))) { //父节点设置为当前节点 x = parentOf(x); //左旋 rotateLeft(x); } setColor(parentOf(x), BLACK);//父节点设置为黑色 setColor(parentOf(parentOf(x)), RED);//爷爷节点设置为红色 rotateRight(parentOf(parentOf(x)));//以爷爷节点为支点右旋 } } else { //表示当前节点的父节点是爷爷节点的右子节点 //就可以用leftOf获取当前节点的叔叔节点 Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } //根节点设置为黑色 root.color = BLACK; }
3、可变参数
方法形参的个数可变,底层就是一个数组
格式:属性类型…名字
注意事项:
- 在方法的形参中只能写一个可变参数
- 在方法中除了可变参数外,还有其他参数,可变参数要写在最后
package TestCode; public class TestClass { public static void main(String[] args) { int sum=0; int res = getSum(sum,1,2); int res1 = getSum(sum,1,2,3,4); System.out.println(res); System.out.println(res1); } public static int getSum(int sum, int...args) { for (int i : args) { sum += i; } return sum; } }
4、集合工具类Collections
方法名称 说明 public static boolean addAll(Collection<? super T> c, T… elements) 批量添加元素 public static void shuffle(List<?> list) 打乱List集合元素的顺序 public static void sort(List list) 排序 public static void sort(List list, Comparator c) 按指定规则排序 public static int binarySearch(List list, T key) 二分查找法查找元素 public static void copy(List dest, List src) 拷贝集合中的元素 public static void fill(List list, T obj) 使用指定的元素填充集合 public static T max/min(Collection coll) 根据默认自然排序获取最大/最小值 public static void swap(List<?> list, int i, int j) 交换集合中指定位置的元素
5、不可变集合
长度和内容不能变的集合
方法名称 说明 static List of(E…elements) 创建一个具有指定元素的List集合对象 static Set of(E…elements) 创建一个具有指定元素的Set集合对象 static <K, V> Map<K, V> of(E…elements) 创建一个具有指定元素的Map集合对象 List<String> list = List.of("aaa", "ccc", "bbb"); Set<String> set = Set.of("a", "b"); /* Map的of方法参数是有上线的,最多只能传递20个参数即10个键值对 */ Map<String, Integer> map = Map.of("zhangsan", 15, "lisi", 20); /* 如果传递多于10个键值对,使用Map.ofEntries */ HashMap<String,Integer> hashMap = new HashMap<>(); hashMap.put("a",1); hashMap.put("b",2); hashMap.put("c",3); hashMap.put("d",4); hashMap.put("e",5); hashMap.put("f",6); hashMap.put("g",7); hashMap.put("h",8); hashMap.put("i",9); hashMap.put("j",10); hashMap.put("k",11); Set<Map.Entry<String, Integer>> entries = hashMap.entrySet(); //把entries变成一个数组 //toArray方法在底层会比较集合与数组长度 //如果集合长度 > 数组长度:数据在数组中放不下,根据实际数据个数重新创建数组 //如果集合长度<= 数组长度:可以直接使用 Map.Entry[] array = entries.toArray(new Map.Entry[0]); //不可变的map集合 Map map = Map.ofEntries(array); System.out.println(map); Map<Object, Object> map1 = Map.ofEntries(hashMap.entrySet().toArray(new Map.Entry[0])); System.out.println(map1); //JDK10之后 Map<String, Integer> map2 = Map.copyOf(hashMap); System.out.println(map2);
6、Stream流
操作步骤:
- 先得到一条Stream流,并把数据放上去
- 利用Stream流中API进行各种操作
获取方式 方法名 说明 单列集合 default Stream stream() Collection中的默认方法 数列集合 无(通过keySet和entrySet先转成单列集合) 无法直接使用Steam流 数组 public static Stream stream(T[] array) Arrays工具类中的静态方法 零散数据 public static Stream of(T…values) Stream接口中的静态方法 package TestCode; import java.util.*; import java.util.function.Consumer; import java.util.stream.Stream; public class TestClass { public static void main(String[] args) { //单列集合 ArrayList<String> arrayList = new ArrayList<>(); Collections.addAll(arrayList, "aaa","bbb","ccc"); /*Stream<String> stream = arrayList.stream(); //使用终结方法打印流数据 stream.forEach(new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } });*/ arrayList.stream().forEach(s -> System.out.println(s)); //双列集合 HashMap<String, Integer> hashMap = new HashMap<>(); hashMap.put("a1",111); hashMap.put("b1",222); hashMap.put("c1",333); hashMap.keySet().stream().forEach(s-> System.out.println(s)); hashMap.entrySet().stream().forEach(s-> System.out.println(s)); //数组 int[] arr = {1,2,3,4}; Arrays.stream(arr).forEach(s-> System.out.println(s)); String[] arr2 = {"a2","b2","c2"}; Arrays.stream(arr).forEach(s-> System.out.println(s)); //零散数据 /* of方法的形参是一个可变参数,可以传递一堆类型相同的零散数据,也可以传递数组 但是数组必须是引用类型,如果是基本类型,会把整个数组当一个元素放到Stream中 */ Stream.of(1,2,4,5).forEach(s-> System.out.println(s)); Stream.of("a3","b3","c3").forEach(s-> System.out.println(s)); } }
中间方法:方法调用完毕之后还可以调用其他方法 eg:过滤、转换
中间方法返回新的Stream流,原来的流只能使用一次,最好使用链式编程
修改Stream流中的数据不会影响原来集合或数组中的数据
名称 说明 Stream filter(Predicate<? super T> predicate) 过滤 Stream limit(long maxSize); 获取前几个元素 Stream skip(long n); 跳过前几个元素 Stream distinct(); 元素去重(依赖hashCode和equals方法) static Stream concat(Stream<? extends T> a, Stream<? extends T> b) 合并a,b两个流为一个流 Stream map(Function<T, R> mapper) 转换流中的数据类型 package TestCode; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; public class TestClass { public static void main(String[] args) { //单列集合 ArrayList<String> arrayList = new ArrayList<>(); Collections.addAll(arrayList, "aaa-1","bbb-2","ccc-4","abc-15","adc-3","bad-9","aaa-6"); //过滤 /*arrayList.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { //如果返回值为true,表示当前数据留下 //如果返回值为false,表示当前数据舍弃 return s.startsWith("a"); } }).forEach(s-> System.out.println(s));*/ arrayList.stream().filter(s -> s.startsWith("a")).forEach(s-> System.out.println(s)); //limit arrayList.stream().limit(3).forEach(s-> System.out.println(s)); //skip arrayList.stream().limit(3).skip(2).forEach(s-> System.out.println(s)); //distinct arrayList.stream().distinct().forEach(s-> System.out.println(s)); //concat Stream.concat(arrayList.stream(),arrayList.stream()).forEach(s-> System.out.println(s)); //第一个类型:流中原本的数据类型 //第二个类型:要转成之后的类型 //返回值:转换之后的数据 /*arrayList.stream().map(new Function<String, Integer>() { @Override public Integer apply(String s) { String ageStr = s.split("-")[1]; int age = Integer.parseInt(ageStr); return age; } }).forEach(s-> System.out.println(s));*/ arrayList.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(s-> System.out.println(s)); } }
终结方法:调用完毕后不能调用其他方法 eg:打印输出
名称 说明 void forEach(Consumer action) 遍历 long count() 统计 toArray() 收集流中的数据,放到数组中 collect(Collector collector) 收集流中的数据,放到集合中 package TestCode; import java.util.*; import java.util.function.Function; import java.util.function.IntFunction; import java.util.stream.Collectors; public class TestClass { public static void main(String[] args) { //单列集合 ArrayList<String> arrayList = new ArrayList<>(); Collections.addAll(arrayList, "aaa-1","bbb-2","ccc-4","abc-15","adc-3","bad-9","aaa-6"); //数组 Object[] array = arrayList.stream().toArray(); System.out.println(Arrays.toString(array)); //toArray方法的参数:负责创建一个指定类型的数组;底层会将流里面的数据都放到数组中;返回一个装着流里所有数据的数组 //IntFunction的泛型:具体类型的数组 //apply的形参:流中数据的个数,要跟数组长度保持一致 /*String[] arr = arrayList.stream().toArray(new IntFunction<String[]>() { @Override public String[] apply(int value) { return new String[value]; } }); System.out.println(Arrays.toString(arr));*/ String[] arr = arrayList.stream().toArray(value -> new String[value]); System.out.println(Arrays.toString(arr)); //List List<String> newList = arrayList.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList()); System.out.println(newList); //Set Set<String> newSet = arrayList.stream().filter(s -> s.startsWith("a")).collect(Collectors.toSet()); System.out.println(newSet); //Map,键不可以重复 Map<String, Integer> map1 = arrayList.stream() .filter(s -> s.startsWith("b")) /* * toMap 参数一表示键的生成规则 * 参数二表示值的生成规则 * * 参数一: * Function泛型一:表示流中每一个数据的类型 * 泛型二:表示Map集合中键的数据类型 * * 方法apply形参:表示流里面的每一个数据 * 方法体:生成键 * 返回值:已生成的键 * 参数二: * Function泛型一:表示流中每一个数据的类型 * 泛型二:表示Map集合中值的数据类型 * * 方法apply形参:表示流里面的每一个数据 * 方法体:生成值 * 返回值:已生成的值 * * */ .collect(Collectors.toMap(new Function<String, String>() { @Override public String apply(String s) { return s.split("-")[0]; } }, new Function<String, Integer>() { @Override public Integer apply(String s) { return Integer.parseInt(s.split("-")[1]); } })); System.out.println(map1); //Map - lambda Map<String, Integer> map2 = arrayList.stream().filter(s -> s.startsWith("b")) .collect(Collectors.toMap(s -> s.split("-")[0], s -> Integer.parseInt(s.split("-")[1]))); System.out.println(map2); } }
7、方法引用
把已经有的方法当作函数式接口中抽象方法的方法体
- 引用处必须是函数式接口
- 被引用的方法必须已经存在
- 被引用方法的形参和返回值需要和抽象方法保持一致
- 被引用的方法的功能要满足当前需求
分类
引用静态方法
类名::静态方法名
package TestCode; import java.util.*; public class TestClass { public static void main(String[] args) { Integer[] arrs = {1,2,3,4,5,6,7,8}; Arrays.sort(arrs, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } }); System.out.println(Arrays.toString(arrs)); //方法引用 Arrays.sort(arrs,TestClass::subtraction); System.out.println(Arrays.toString(arrs)); } public static int subtraction(int num1, int num2) { return num2-num1; } }
引用成员方法
对象::方法名
引用其他类的成员方法
其他类对象::方法名
package TestCode; import java.util.*; import java.util.function.Predicate; public class TestClass { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); Collections.addAll(list, "aaa","bbb","adc","ac"); list.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { return s.startsWith("a") && s.length() == 3; } }).forEach(s-> System.out.println(s)); //方法引用 StrOperation so = new StrOperation(); list.stream().filter(so::strOp).forEach(s-> System.out.println(s)); } }
package TestCode; public class StrOperation { public boolean strOp (String s) { return s.startsWith("a") && s.length() == 3; } }
引用本类的成员方法 - 引用处不能是静态方法
this::方法名
引用父类的成员方法 - 引用处不能是静态方法
super::方法名
引用构造方法 - 创建对象
类名::new
package TestCode; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; public class TestClass { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); Collections.addAll(list, "aaa-11","bbb-12","adc-13","acd-14"); List<Student> collect = list.stream().map(new Function<String, Student>() { @Override public Student apply(String s) { String[] str = s.split("-"); String name = str[0]; int age = Integer.parseInt(str[1]); return new Student(name, age); } }).collect(Collectors.toList()); System.out.println(collect); //方法引用 - 其实是使用该构造方法Student(String s) List<Student> collect1 = list.stream().map(Student::new).collect(Collectors.toList()); System.out.println(collect1); } }
package TestCode; public class Student { private String name; private int age; public Student() { } public Student(String s){ String[] str = s.split("-"); this.name = str[0]; this.age = Integer.parseInt(str[1]); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
其他调用方式
使用类名引用成员方法
- 引用处必须是函数式接口
- 被引用的方法必须已经存在
- 被引用方法的形参需要和抽象方法第二个形参到最后一个形参保持一致,返回值保持一致
- 被引用的方法的功能要满足当前需求
package TestCode; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; public class TestClass { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); Collections.addAll(list, "aaa-11","bbb-12","adc-13","acd-14"); /* 抽象方法apply形参的详解: 第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法 假设流里面的数据是字符串类型 即apply(String s),则只能引用String类的方法 第二个参数到最后一个参数:和被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要无参的成员方法 eg:apply(String s)没有第二个参数,只能引用String类中无参的成员方法 */ list.stream().map(new Function<String, String>() { @Override public String apply(String s) { return s.toUpperCase(); } }).forEach(s-> System.out.println(s)); //方法引用 list.stream().map(String::toUpperCase).forEach(s-> System.out.println(s)); } }
引用数组的构造方法 - 创建数组
数组的类型要保持和流中的数据类型一致
package TestCode; import java.util.*; import java.util.function.IntFunction; public class TestClass { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); Collections.addAll(list, 1,2,3,4,5); Integer[] array = list.stream().toArray(new IntFunction<Integer[]>() { @Override public Integer[] apply(int value) { return new Integer[value]; } }); System.out.println(Arrays.toString(array)); //方法引用 Integer[] array1 = list.stream().toArray(Integer[]::new); System.out.println(Arrays.toString(array1)); } }
8、集合嵌套
package RollCall;
import java.util.*;
public class RollCall4 {
public static void main(String[] args) {
HashMap<String, ArrayList<String>> hm = new HashMap<>();
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"南京市","杭州市","无锡市");
hm.put("江苏省",list);
Set<Map.Entry<String, ArrayList<String>>> entries = hm.entrySet();
for (Map.Entry<String, ArrayList<String>> entry : entries) {
String key = entry.getKey();
ArrayList<String> city = entry.getValue();
StringJoiner sj = new StringJoiner(", ","","");
for (String c : city) {
sj.add(c);
}
System.out.println(key + " = " + sj);
}
}
}
二、异常
作用:
用来查询bug的关键参考信息
可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况
try…catch 捕获异常 - 程序不会停止
//JDK7之前 try{ 可能出现异常的代码 }catch(异常类名 变量名) { 异常的处理代码 }catch(异常类名 变量名) { 异常的处理代码 }finally{ 此代码一定会被执行,除非虚拟机停止 } //JDK7之后 try{ 可能出现异常的代码 }catch(异常类名|异常类名 变量名) { 异常的处理代码 }finally{ 此代码一定会被执行,除非虚拟机停止 }
注意:
- 如果try中没有遇到问题,把try中所有代码全部执行完毕,不会执行catch里的代码
- 如果try中遇到多个问题,会写多个catch与之对应;如果多个异常中存在父子关系,父类要写在最下面
- 如果try中遇到问题,try下面其他代码不会被执行
常见方法:
方法名称 说明 public String getMessage() 返回此throwable的详细消息字符串 public String toString() 返回此可抛出的简短描述 public void printStackTrace() 把异常的错误信息输出在控制台,仅仅打印信息,不会停止程序 抛出处理
throws throw 写在方法定义处,表示声明一个异常 写在方法内,结束方法,手动抛出异常 public void 方法名() throws 异常类名1,异常类名2…{…} public void 方法名() { throw new NullPointerException();} 编译时异常:必须要写;运行时异常:可以不写 自定义异常
- 定义异常类
- 写继承关系
- 空参构造
- 有参构造
package TestCode; public class NameFormatException extends RuntimeException{ public NameFormatException() { } public NameFormatException(String message) { super(message); } }
三、File
构造方法名称 说明 public File(String pathname) 根据文件路径创建文件对象 public File(String parent, String child) 根据父路径名字符串和子路径名字符串创建文件对象 public File(File parent, String child) 根据父路径对应文件对象和子路径名字符串创建文件对象 package TestCode; import java.io.File; public class TestClass { public static void main(String[] args) { String pathname = "C:\\Users\\lzkj\\Desktop\\1.pdf"; File f1 = new File(pathname); System.out.println(f1); //父级路径 String parent = "C:\\Users\\lzkj\\Desktop"; //子级路径 String child = "1.pdf"; File f2 = new File(parent, child); System.out.println(f2); File parent2 = new File("C:\\Users\\lzkj\\Desktop"); String child2 = "1.pdf"; File f3 = new File(parent2, child2); System.out.println(f3); } }
成员方法
判断、获取方法名称 说明 public boolean isDirectory() 判断此路径名表示的File是否为文件夹 public boolean isFile() 判断此路径名表示的File是否为文件 public boolean exists() 判断此路径名表示的File是否存在 public long length() 返回文件的大小(单位:字节)只能获取文件不能获取文件夹 public String getAbsolutePath() 返回文件的绝对路径 public String getPath() 返回定义文件时使用的路径 public String getName() 返回文件的名称(带后缀)或文件夹的名字 public long lastModified() 返回文件的最后修改时间(毫秒)
创建、删除方法名称 说明 public boolean creatNewFile() 创建一个新的空的文件(不存在,创建成功返回true;存在,创建失败;父级路径不存在会有异常;如果路径不包含后缀名则创建一个没有后缀的文件) public boolean mkdir() 创建单级文件夹(windows路径是唯一的) public boolean mkdirs() 创建单级和多级文件夹 public boolean delete() 删除文件、空文件夹(不走回收站)
获取并遍历方法名称 说明 public File[] listFiles() 获取当前该路径下的所有内容的路径
1、路径不存在时,返回null
2、路径是文件,返回null
3、路径是空文件夹,返回一个长度为0的数组
4、路径是一个有内容的文件夹,返回该文件夹内所有的文件和文件夹路径的File数组
5、路径是一个有隐藏文件的文件夹,返回该文件夹内所有的文件和文件夹路径的File数组,包含隐藏文件夹
6、路径是一个需要权限访问的文件夹,返回nullpublic String[] list() 获取当前该路径下的所有内容的名字 package TestCode; import java.io.File; public class TestClass { public static void main(String[] args) { File file = new File("C:\\Users\\lzkj\\Desktop"); findFile(file); } public static void findFile(File file){ File[] files = file.listFiles(); if (files != null){ for (File f : files) { if(f.isFile()){ String name = f.getName(); if (name.endsWith(".docx")){ System.out.println(f); } }else{ findFile(f); } } } } }
四、I/O流
1、基本
分类:
流的方向
- 输入流:读取本地文件数据
- 输出流:数据写入本地文件
操作文件类型
字节流:所有类型文件 InputStream - 字节输入流;OutputStream - 字节输出流
不要读取文本文件,遇到中文容易乱码
字符流:纯文本文件 Reader - 字符输入流;Writer - 字符输出流
作用:读写数据
2、字节流
/*FileOutputStream*/ package TestCode; import java.io.FileOutputStream; import java.io.IOException; public class TestClass { public static void main(String[] args) throws IOException { //FileOutputStream:操作本地文件的字节输出流,把程序中数据写到本地文件 /* * 创建字节输出流对象 * 参数是字符串表示的路径或者File对象 * 如果文件不存在会创建一个新文件,但要保证父级路径存在 * 如果文件存在会清空文件 */ //续写创建FileOutputStream()对象第二个参数为true则续写,false则清空文件,默认false FileOutputStream fileOutputStream = new FileOutputStream("a.txt"); /* * 写出数据 * write方法参数是整数,对应ASCII码值 */ fileOutputStream.write(97); byte[] bytes = {98,99,100,101,102}; fileOutputStream.write(bytes); //参数二:起始索引 参数三;个数 fileOutputStream.write(bytes,1,3); String str = "adhuedhwiuehdoweu"; byte[] bytes1 = str.getBytes(); fileOutputStream.write(bytes1); //换行 windows - \r\n或\r或\n; Linux - \n; Mac - \r String str1 = "\r\n"; byte[] bytes2 = str1.getBytes(); fileOutputStream.write(bytes2); /* * 释放资源 * 每次使用之后都要释放资源 */ fileOutputStream.close(); } }
/*FileInputStream*/ package TestCode; import java.io.FileInputStream; import java.io.IOException; public class TestClass { public static void main(String[] args) throws IOException { /* * FileInputStream输入流 * 如果文件不存在会直接报错 */ FileInputStream fileInputStream = new FileInputStream("a.txt"); //read一次读取一个字节,是数据在ASCII上的数字;读到文件末尾,返回-1 int b = fileInputStream.read(); System.out.println((char)b); int b1; while ((b1=fileInputStream.read())!=-1){ System.out.println((char)b1); } fileInputStream.close(); } }
/*文件拷贝*/ package TestCode; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class TestClass { public static void main(String[] args) throws IOException { /* * 文件拷贝(小文件) */ FileInputStream fileInputStream = new FileInputStream("a.txt"); FileOutputStream fileOutputStream = new FileOutputStream("b.txt"); int b; while((b = fileInputStream.read()) != -1) { fileOutputStream.write(b); } fileOutputStream.close(); fileInputStream.close(); /* * 文件拷贝(大文件) */ //一次读一个字节数组数据 长度为1024的整数倍 FileInputStream fis = new FileInputStream("a.txt"); FileOutputStream fos = new FileOutputStream("c.txt"); int len; byte[] bytes = new byte[1024]; //一次读取数小于等于组长度个数据,返回本次读取的字节数 while((len = fis.read(bytes)) != -1){ //读多少写多少 fos.write(bytes, 0 , len); } fos.close(); fis.close(); } }
运行时异常
package TestCode; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class TestClass { public static void main(String[] args){ FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream("a.txt"); fos = new FileOutputStream("c.txt"); int len; byte[] bytes = new byte[1024]; //一次读取数小于等于组长度个数据,返回本次读取的字节数 while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0 , len); } } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null){ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } } //------------------------------------代码简化----------------------------------------- package TestCode; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class TestClass { public static void main(String[] args) throws FileNotFoundException { /* JDK7 只有实现AutoCloseable接口的类,才能在try后的小括号中创建对象 try(创建流对象1;创建流对象2){ }catch(异常类名 变量名){ } */ try(FileInputStream fis = new FileInputStream("a.txt"); FileOutputStream fos = new FileOutputStream("c.txt")) { int len; byte[] bytes = new byte[1024]; //一次读取数小于等于组长度个数据,返回本次读取的字节数 while((len = fis.read(bytes)) != -1){ fos.write(bytes, 0 , len); } } catch (IOException e) { e.printStackTrace(); } /* JDK9 创建流对象1; 创建流对象2; try(流1; 流2){ }catch(异常类名 变量名){ } */ FileInputStream fis1 = new FileInputStream("a.txt"); FileOutputStream fos1 = new FileOutputStream("d.txt"); try(fis1;fos1) { int len; byte[] bytes = new byte[1024]; //一次读取数小于等于组长度个数据,返回本次读取的字节数 while((len = fis1.read(bytes)) != -1){ fos1.write(bytes, 0 , len); } } catch (IOException e) { e.printStackTrace(); } } }
3、字符集
ASCII字符集:
- 一个英文占一个字节
GBK字符集:
- 一个英文占一个字节,二进制第一位是0
- 一个汉字占两个字节 二进制高位字节的第一位是1
Unicode字符集:
UTF-8编码方式:1~4个字节保存
一个英文占一个字节 高位补0
一个汉字占三个字节 第一个字节补1110,第二个补10,第三个补10
eg: 汉 27721 -> 0110//1100 01//001001 ->11100110 10110001 10001001
乱码原因:
- 读取数据时未读完整个汉字
- 编码和解码方式不统一
String类中编码方法 说明 public byte[] getBytes() 使用默认方式编码 public byte[] getBytes(String charsetName) 使用指定方式编码 String类中解码方法 说明 public String(byte[] bytes) 使用默认方式解码 public String(byte bytes[], String charsetName) 使用指定方式解码
4、字符流
字符流 = 字节流 + 字符集
/*FileReader*/ package TestCode; import java.io.FileReader; import java.io.IOException; public class TestClass { public static void main(String[] args) throws IOException { FileReader fr = new FileReader("a.txt"); //FileReader fr = new FileReader("count.txt", Charset.forName("GBK")); int ch; /* * read()默认一个一个字节的读取,中文则一次会读取多个 * 读取之后,方法底层会进行解码并转成十进制,并返回这个十进制 */ while((ch = fr.read()) != -1){ System.out.print((char)ch); } fr.close(); System.out.println(""); FileReader fr1 = new FileReader("a.txt"); /* * 有参read方法 public int read(char[] cbuf) * 读取数据、解码、强转合并,强转之后的数据放到数组中 */ char[] c = new char[2]; int len; while((len = fr1.read(c)) != -1){ System.out.print(new String(c,0,len)); } fr1.close(); } }
/* FileWriter 数据先存放到缓冲区,当数据>8192,缓冲区装满时,或者手动调用flush,或者关闭资源时才会把缓冲区数据写入文件中 flush 在刷新之后还可以继续往文件中写出数据,没有断开通道 close 断开通道,无法继续写入数据到文件中 */ package TestCode; import java.io.FileWriter; import java.io.IOException; public class TestClass { public static void main(String[] args) throws IOException { FileWriter fw = new FileWriter("a.txt"); // FileWriter fw = new FileWriter("a.txt",true); fw.write(27721); fw.write("你好"); char[] c = {'a','b','c','我'}; fw.write(c); fw.close(); } }
FileReader底层原理
/*拷贝文件夹*/
package TestCode;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestClass {
public static void main(String[] args) throws IOException {
File src = new File("aaa");
File dest = new File("dest");
copyDir(src, dest);
}
private static void copyDir(File src, File dest) throws IOException {
dest.mkdirs();
//进入数据源
File[] files = src.listFiles();
//遍历文件
for (File file : files) {
if (file.isFile()){
//如果是文件,拷贝
FileInputStream fls = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(new File(dest, file.getName()));
byte[] bytes = new byte[1024];
int len;
while((len = fls.read(bytes)) != -1){
fos.write(bytes, 0 ,len);
}
fos.close();
fls.close();
}else{
//如果是文件夹, 递归
copyDir(file, new File(dest, file.getName()));
}
}
}
}
5、字节缓冲流
字节缓冲流:BufferedInputStream字节缓冲输入流、BufferedOutputStream字节缓冲输出流
字符缓冲流:BufferedReader字符缓冲输入流、BufferedWriter字符节缓冲输出流
字节缓冲流
底层自带长度为8192的缓冲区提高性能
方法名称 说明 public BufferedInputStream(InputStream is) 把基本流包装成高级流,提高读取数据的性能 public BufferedOutputStream(OutputStream os) 把基本流包装成高级流,提高写出数据的性能 package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException { BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.txt")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt")); int len; byte[] bytes = new byte[1024]; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } bos.close(); bis.close(); } }
底层原理
字符缓冲流
字符缓冲输入流特有方法 说明 public String readLine() 读取一行数据,如果没有数据返回null 字符缓冲输出流特有方法 说明 public void newLine() 跨平台的换行 package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException { /* 实现一个验证程序运行次数的小程序,要求如下: 1.当程序运行超过3次时给出提示:本软件只能免费使用3次,欢迎您注册会员后继续使用~ 2.程序运行演示如下: 第一次运行控制台输出: 欢迎使用本软件,第1次使用免费~ 第二次运行控制台输出: 欢迎使用本软件,第2次使用免费~ 第三次运行控制台输出: 欢迎使用本软件,第3次使用免费~ 第四次及之后运行控制台输出:本软件只能免费使用3次,欢迎您注册会员后继续使用~ */ //1.把文件中使用次数读取到内存里 BufferedReader br = new BufferedReader(new FileReader("count.txt")); String line = br.readLine(); br.close(); int count = Integer.parseInt(line); count++; //2.对数字进行判断 if (count <= 3) { System.out.println("欢迎使用本软件,第" + count + "次使用免费~"); }else { System.out.println("本软件只能免费使用3次,欢迎您注册会员后继续使用~"); } //3.将最新的使用次数保存到文件中 BufferedWriter bw = new BufferedWriter(new FileWriter("count.txt")); bw.write(count + ""); bw.close(); } }
6、转换流
是字符流和字节流之间的桥梁,输入时字节流转换成字符流,输出时字符流转换成字节流
字符转换输入流:InputStreamReader
字符转换输出流:OutputStreamReader
package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("b.txt"))); String line; while ((line = br.readLine()) != null) { System.out.println(line); } br.close(); } }
7、序列化流、反序列化流
序列化流/对象操作输出流:将java中的对象写到本地文件当中
构造方法 说明 public ObjectOutputStream(OutputStream out) 将基本流包装成高级流 成员方法 说明 public final void writeObject(Object obj) 把对象序列化到文件去 package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException { Student student = new Student("zhangsan",23); ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("a.txt")); //oos.writeObject(student)会出现NotSerializableException异常 //解决办法:让Javabean类实现Serializable接口 //Serializable接口没有抽象方法,是标记型接口,表示当前类可以被序列化 oos.writeObject(student); oos.close(); } }
反序列化流/对象操作输入流:把序列化到本地文件中的对象,读取到程序中
构造方法 说明 public ObjectInputStream(InputStream out) 将基本流包装成高级流 成员方法 说明 public Object readObject() 把序列化到本地文件中的对象,读取到程序中 package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream ois =new ObjectInputStream(new FileInputStream("a.txt")); Student o = (Student) ois.readObject(); System.out.println(o); ois.close(); } }
Javabean类固定版本号(序列化对象后修改Javabean类再次反序列化出现InvalidClassException异常)
方法一:手写
private static final long serialVersionUID = 1L;
方法二:修改IDEA设置
如果不显示
成员变量不想序列化到本地
transient:瞬态关键字
eg:private transient int age;/*读写多个对象*/ package TestCode; import java.io.Serial; import java.io.Serializable; public class Student implements Serializable { @Serial private static final long serialVersionUID = 94972375498334644L; private String name; private int age; private transient String address; public Student() { } public Student(String name, int age, String address) { this.name = name; this.age = age; this.address = address; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", address='" + address + '\'' + '}'; } } //------------------------------------------------------------------------------ package TestCode; import java.io.*; import java.util.ArrayList; import java.util.Collections; public class TestClass { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt")); Student stu1 = new Student("zhangsan",23,"北京"); Student stu2 = new Student("lisi",24,"广州"); Student stu3 = new Student("wangwu",25,"上海"); ArrayList<Student> list = new ArrayList<>(); Collections.addAll(list,stu1,stu2,stu3); oos.writeObject(list); oos.close(); } } //--------------------------------------------------------------------------------- package TestCode; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.util.ArrayList; public class TestClass1 { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt")); ArrayList<Student> list = (ArrayList<Student>) ois.readObject(); for (Student student : list) { System.out.println(student); } } }
8、字节、字符打印流
字节打印流:PrintStream / 字符打印流:PrintWriter
构造方法 说明 public PrintStream(OutputStream/File/String) 关联字节输出流/文件/文件路径 public PrintStream(String fileName, Charset charset) 指定字符编码
成员方法 说明 public void write(int b) 常规方法 public void println(Xxx xx) 特有方法:打印任意数据,自动刷新,自动换行 public void print(Xxx xx) 特有方法:打印任意数据,不换行 public void printf(String format, Object … args) 特有方法:带有占位符的打印,不换行 package TestCode; import java.io.*; import java.nio.charset.Charset; import java.util.Date; public class TestClass { public static void main(String[] args) throws IOException, ClassNotFoundException { PrintStream ps = new PrintStream("a.txt", Charset.forName("utf-8")); //% n表示换行 ps.printf("我叫%s %n","阿玮"); ps.printf("%s喜欢%s %n","阿珍","阿强"); ps.printf("字母H的大写: %c %n",'H'); ps.printf("8>3的结果是: %b %n", 8 > 3); ps.printf("100的一半是: %d %n",100 / 2); ps.printf("100的16进制数是: %x %n",100); ps.printf("100的8进制数是: %o %n",100); ps.printf("50元的书打8.5折扣是: %f元%n",50 * 0.85); ps.printf("计算的结果转16进制: %a %n",50 * 0.85); ps.printf("计算的结果转科学计数法表示: %e %n",50 * 0.85); ps.printf("计算的结果转成指数和浮点数,结果的长度较短的是: %g %n",50 *0.85); ps.printf("带有百分号的符号表示法,以百分之85为例: %d%% %n",85); ps.println("----------"); double num1 = 1.; ps.printf("num: %.4g %n", num1); ps.printf("num: %.5g %n", num1); ps.printf("num: %.6g %n", num1); float num2 = 10.F; ps.printf("num: %.4f %n", num2); ps.printf("num: %,5f %n", num2); ps.printf("num: %.6f %n", num2); ps.println("--------------"); ps.printf("数字前面带有@的表示友式: %03d %n",7); ps.printf("数宁前面带有@的表示方式: %04d %n",7); ps.printf("数字前面带有空格的表示方式: % 8d %n",7); ps.printf("整数分组的效果是: %,d %n",9989997); ps.println("------------------"); //最终结果是10位,小数点后面是5位,不够在前面补空格,补满19位 // 如果实际数字小数点后面过长,但是只规定两位,会四舍五入 // 如果整数部分过长,超出规定的总长度,会以实际为准 ps.printf("一本书的价格是: %2.5f元%n",49.8); ps.printf("%(f%n",-76.04); //%f,默认小数点后面7位, //<.表示采取跟前面一样的内容 ps.printf("%f和%3.2f %n", 86.04,1.789651); ps.printf("%f和%<3.2f %n",86.04,1.789651); ps.println("---------"); Date date = new Date(); // %t 表示时间,但是不能单独出现,要指定时间的格式 // %tc 周二 12月 06 22:08:4 CST 2022 // %tD 斜线隔开 // %tF 冒号隔开 (12小时制) // %tr 冒号隔开 (24小时制) // %tT 冒号隔开(24小时制,带时分秒) ps.printf("全部日期和时间信息: %tc %n",date); ps.printf("月/日/年格式: %tD %n",date); ps.printf("年-月-日格式: %tF %n",date); ps.printf("HH:MM:SS PM格式(12时制): %tr n",date); ps.printf("HH:MM格式(24时制): %tR %n",date); ps.printf("HH:MM:SS格式(24时制): %tT %n",date); ps.println("-------------"); ps.printf("星期的简称: %ta %n",date); ps.printf("星期的全称: %tA %n",date); ps.printf("英文月份简称: %tb %n",date); ps.printf("英文月份全称: %tB %n",date); ps.printf("年的前两位数字(不足两位前面补e): %tC %n",date); ps.printf("年的后两位数字(不足两位前面补@): %ty %n",date); ps.printf("一年中的第几天: %tj %n",date); ps.printf("两位数字的月份(不足两位前面补e): %tm %n",date); ps.printf("两位数字的日(不足两位前面补e): %td %n",date); ps.printf("月份的日(前面不补e): %te %n",date); ps.println("-------------"); ps.printf("两位数字24时制的小时(不是2位前面补0):%tH %n",date); ps.printf("两位数字12时制的小时(不是2位前面补0):%tI %n",date); ps.printf("两位数字24时制的小时(前面不补0):%tk %n",date); ps.printf("两位数字12时制的小时(前面不补0):%tl %n",date); ps.printf("两位数字的分钟(不足2位前面补0):%tM %n",date); ps.printf("两位数字的秒(不是2位前面补0):%ts %n",date); ps.printf("三位数字的毫秒(不足3位前面补0):%tL %n",date); ps.printf("九位数字的毫秒数(不足9位前面补0):%tN %n",date); ps.printf("小写字母的上午或下午标记(英): %tp %n",date); ps.printf("小写字母的上午或下午标记(中): %tp %n",date); ps.printf("相对于GMT的偏移量:%tz %n",date); ps.printf("时区缩写字符串:%tZ%n",date); ps.printf("1970-1-1 00:00:00 到现在所经过的秒数: %ts %n",date); ps.printf("1970-1-1 00:00:00 到现在所经过的毫秒数: %tQ %n",date); ps.close(); } }
System.out.println()详解
package TestCode; import java.io.*; public class TestClass { public static void main(String[] args) throws IOException, ClassNotFoundException { /* 获取System类中静态成员变量out public static final PrintStream out = null; out是字节打印流,可以调用println("")方法 */ //特殊的打印流,系统中的标准输出流,不可以关闭,在系统中是唯一的 PrintStream ps = System.out; ps.println("---"); //等价于 - 链式编程 System.out.println("---"); } }
9、解压缩流、压缩流
压缩包里的每一个文件 - ZipEntry,解压的本质:把每一个ZipEntry按照层级拷贝到本地另一个文件夹中
/*解压缩*/ package TestCode; import java.io.*; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class TestClass { public static void main(String[] args) throws IOException{ File src = new File("D:\\Learning_Workspace\\Learning_java\\Code\\testProject\\aa.zip"); File dest = new File(src.getParent()); unzip(src,dest); } private static void unzip(File src, File dest) throws IOException { ZipInputStream zip = new ZipInputStream(new FileInputStream(src)); ZipEntry entry; while ((entry = zip.getNextEntry()) != null){ System.out.println("entry.toString: " + entry.toString()); if(entry.isDirectory()){ //文件夹:在目的地dest处创建一个同样的文件夹 File file = new File(dest, entry.toString()); file.mkdirs(); }else{ //文件:需要读取到压缩包中的文件,并存放到目的地dest中(按照层级目录进行存放) FileOutputStream fos = new FileOutputStream(new File(dest, entry.toString())); int b; while((b = zip.read())!=-1){ fos.write(b); } fos.close(); //表示在压缩包中的一个文件处理完毕 zip.closeEntry(); } } zip.close(); } }
/*压缩*/ package TestCode; import java.io.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class TestClass { public static void main(String[] args) throws IOException{ File src = new File("D:\\Learning_Workspace\\Learning_java\\Code\\testProject\\aa"); File dest = new File(src.getParentFile(),src.getName()+".zip"); ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest)); toZip(src,zos, src.getName()); zos.close(); } /* * 获取src里每一个文件变成ZipEntry对象,放入到压缩包中 * 参数一:数据源 * 参数二:压缩流 * 参数三:压缩包内部的路径 */ private static void toZip(File src, ZipOutputStream zos, String name) throws IOException { File[] files = src.listFiles(); for (File file : files) { if (file.isFile()){ //变成ZipEntry对象,放入压缩包中 ZipEntry entry = new ZipEntry(name + "\\" + file.getName()); zos.putNextEntry(entry); FileInputStream fis = new FileInputStream(file); int b; while((b=fis.read()) != -1){ zos.write(b); } fis.close(); zos.closeEntry(); }else{ toZip(file,zos,name + "\\" + file.getName()); } } } }
10、常用工具包
10.1、Commons-io
步骤:
- 项目中创建lib文件夹 - 存放第三方jar包
- 右击点击jar包,选择Add as Library -> OK
下载路径:https://commons.apache.org/proper/commons-io/download_io.cgi
10.2、hutool
参考路径:https://www.hutool.cn/
五、多线程
进程:程序的基本执行实体
线程:操作系统能够进行运算调度的最小单位,被包含在进程中,是进程中的实际运作单位(eg:应用软件中相互独立且可以同时运行的功能)
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
1、多线程实现方式
继承Thread类的方式进行实现
package TestCode; public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + "HelloWorld"); } } }
package TestCode; public class TestClass { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }
实现Runnable接口的方式进行实现
package TestCode; public class MyRun implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { //获取当前线程对象 System.out.println(Thread.currentThread().getName() + "HelloWorld"); } } }
package TestCode; public class TestClass { public static void main(String[] args) { //创建MyRun对象,表示多线程执行的任务 MyRun mr = new MyRun(); //创建线程对象 Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); t1.setName("线程1"); t2.setName("线程2"); //开启线程 t1.start(); t2.start(); } }
利用Callable接口和Future接口方式实现 - 可以获取到多线程运行的结果
- 创建一个类实现Callable接口,泛型是结果的类型
- 重写call(是有返回值的)
- 创建这个类的对象(表示多线程要执行的任务)
- 创建FutureTask的对象(管理多线程运行的结果)
- 创建Thread对象并启动
package TestCode; import java.util.concurrent.Callable; public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum =0 ; for (int i = 0; i < 100; i++) { sum += i; } return sum; } }
package TestCode; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TestClass { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建MyCallable对象,表示多线程执行的任务 MyCallable mc = new MyCallable(); //创建FutureTask对象 FutureTask<Integer> ft = new FutureTask<>(mc); //创建线程对象 Thread t1 = new Thread(ft); t1.start(); //获取结果 Integer result = ft.get(); System.out.println(result); } }
2、成员方法
线程优先级:线程优先级越大 抢到线程概率越大
package TestCode; public class MyRunnable extends Thread implements Runnable { public MyRunnable() { } public MyRunnable(String name) { super(name); } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + Thread.currentThread().getPriority()); } } }
package TestCode; public class TestClass { public static void main(String[] args){ MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr,"线程1"); Thread t2 = new Thread(mr,"线程2"); t1.setPriority(1); t2.setPriority(10); t1.start(); t2.start(); } }
守护线程 - 非其他守护线程执行完毕,守护线程会陆续结束
package TestCode; public class MyThread1 extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "_" + i); } } }
package TestCode; public class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + "@" + i); } } }
package TestCode; public class TestClass { public static void main(String[] args){ MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.setName("线程1"); t2.setName("线程2"); t2.setDaemon(true); t1.start(); t2.start(); } }
礼让线程
public static void yield()
插入线程
public final void join()
3、生命周期
4、安全问题
同步代码块:把操作共享数据的代码锁起来
格式:
synchronized ( 锁对象 ){ 操作共享数据的代码 }
特点:
- 锁默认打开,有一个线程进去,锁自动关闭
- 里面代码全部执行完毕,线程出来,锁自动打开
package TestCode; public class MyThread extends Thread{ static int ticket = 0; // static Object obj = new Object(); @Override public void run() { while(true){ //锁对象一定要是唯一的 // synchronized (obj){ synchronized (MyThread.class){ if (ticket < 100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket++; System.out.println(getName() + "正在卖第" + ticket + "张票!"); }else { break; } } } } }
package TestCode; public class TestClass { public static void main(String[] args){ MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); MyThread mt3 = new MyThread(); mt1.setName("第一个窗口"); mt2.setName("第二个窗口"); mt3.setName("第三个窗口"); mt1.start(); mt2.start(); mt3.start(); } }
同步方法:把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(参数){...}
特点:
- 锁在方法里所有的代码
- 锁对象不能自己指定 非静态:this / 静态:当前类的字节码文件对象
package TestCode; public class MyThread extends Thread{ static int ticket = 0; @Override public void run() { while(true){ if (method()) break; } } private synchronized boolean method() { if (ticket < 100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket++; System.out.println(getName() + "正在卖第" + ticket + "张票!"); }else { return true; } return false; } }
Lock锁
是接口不能被实例化 需要采用实现类ReentrantLock来实例化()
package TestCode; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MyThread extends Thread{ static int ticket = 0; //static - 所有对象共用同一把锁 static Lock lock = new ReentrantLock(); @Override public void run() { while(true){ lock.lock(); try { if (ticket < 100){ Thread.sleep(10); ticket++; System.out.println(getName() + "正在卖第" + ticket + "张票!"); }else { break; } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
5、生产者和消费者(等待唤醒机制)
方法名称 说明 void wait() 当前线程等待,直到被其他线程唤醒 void notify() 随机唤醒单个线程 void notifyAll() 唤醒所有线程 /*消费者*/ package TestCode; public class Foodie extends Thread{ @Override public void run() { while(true){ synchronized (Desk.lock){ if(Desk.count == 0){ break; }else{ //先判断桌子上是否有面条 if(Desk.foodflag ==0){ //没有就等待 try { Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } }else{ //吃的总数-1 Desk.count--; //有就吃 System.out.println("正在吃,还能再吃" + Desk.count + "碗"); //吃完唤醒厨师继续做 Desk.lock.notifyAll(); //修改桌子的状态 Desk.foodflag = 0; } } } } } }
/*生产者*/ package TestCode; public class Cook extends Thread { @Override public void run() { while (true) { synchronized (Desk.lock) { if (Desk.count == 0){ break; }else { //判断桌子上是否有食物 if (Desk.foodflag == 1) { //如果有,就等待 try { Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { //如果没有,就制作 System.out.println("厨师做了一碗面条"); //修改桌子上的食物状态 Desk.foodflag = 1; //叫醒等待的消费者吃 Desk.lock.notifyAll(); } } } } } }
/*控制线程执行*/ package TestCode; public class Desk { //是否有面条 0:没有面条,1:有面条 public static int foodflag = 0; //总个数 public static int count = 10; //锁对象 public static Object lock = new Object(); }
package TestCode; public class TestClass { public static void main(String[] args){ Cook c =new Cook(); Foodie f = new Foodie(); c.setName("厨师"); f.setName("吃货"); c.start(); f.start(); } }
阻塞队列
阻塞:
- put数据:放不进去,等着
- take数据:取出第一个数据,取不到等着
继承结构:
接口:Iterable、Collection、Queue、BlockingQueue
实现类:ArrayBlockingQueue、底层是数组,有界
LinkedBlockingQueue 底层是链表,无界(并不是真正的无界,最大值int的最大值)
package TestCode; import java.util.concurrent.ArrayBlockingQueue; public class Cook extends Thread{ ArrayBlockingQueue<String> queue; public Cook(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){ try { queue.put("面条"); System.out.println("厨师放一碗面条"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
package TestCode; import java.util.concurrent.ArrayBlockingQueue; public class Foodie extends Thread{ ArrayBlockingQueue<String> queue; public Foodie(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){ try { String food = queue.take(); System.out.println(food); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
package TestCode; import java.util.concurrent.ArrayBlockingQueue; public class TestClass { public static void main(String[] args){ //生产者和消费者必须使用同一个阻塞队列 //1、创建阻塞队列的对象 ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(2); //2、创建线程的对象,并把阻塞队列传递过去 Cook c = new Cook(queue); Foodie f = new Foodie(queue); c.start(); f.start(); } }
6、多线程6种状态
7、线程池
原理:
- 创建一个空池子
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程
- 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
方法名称 说明 public static ExecutorService newCachedThreadPool() 创建一个没有上线的线程池 public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上线的线程池 package TestCode; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestClass { public static void main(String[] args){ // ExecutorService pool = Executors.newCachedThreadPool(); ExecutorService pool = Executors.newFixedThreadPool(3); pool.submit(new MyThread()); pool.submit(new MyThread()); pool.submit(new MyThread()); pool.submit(new MyThread()); } }
自定义线程池
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数:
- 核心线程数量(不能小于0)
- 最大线程数量(最大数量>=核心线程数)
- 空闲时间(值)(不能小于0)
- 空闲时间(单位)(用TimeUnit指定)
- 阻塞队列(不能为null)
- 创建线程的方式(不能为null)
- 要执行的任务过多时的解决方案(不能为null)
临时线程:核心线程都在执行任务,阻塞队列已经排满了,才会创建临时线程
任务拒绝策略 - 内部类 说明 ThreadPoolExecutor.AbortPolicy 默认策略:丢弃任务并抛出RejectedExecutionException异常 ThreadPoolExecutor.DiscardPolicy 丢弃任务,但不抛出异常 ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中 ThreadPoolExecutor.CallerRunsPolicy 调用任务run()方法绕过线程池直接执行 package TestCode; import java.util.concurrent.*; public class TestClass { public static void main(String[] args){ ThreadPoolExecutor pool = new ThreadPoolExecutor( 3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); pool.submit(new MyThread()); pool.submit(new MyThread()); pool.submit(new MyThread()); } }
最大并行数
package TestCode; public class TestClass { public static void main(String[] args){ int count = Runtime.getRuntime().availableProcessors(); System.out.println(count); } }
线程池大小:
CPU密集型运算 : 最大并行数 + 1
I/O密集型运算:最大并行数 * 期望CPU利用率 * [总时间(CPU 计算时间 + 等待时间) / CPU 计算时间]
thread dump - 计算时间
注:
参考视频:
https://www.bilibili.com/video/BV17F411T7Ao?p=1&vd_source=a89593e8d33b31a56b894ca9cad33d33
https://www.bilibili.com/video/BV1yW4y1Y7Ms?p=1&vd_source=a89593e8d33b31a56b894ca9cad33d33