JavaSE基础加强(java_4)

发布时间 2023-10-12 12:08:30作者: _Pomelo

JavaSE基础加强-笔记4

Set系列集合

Set系列集系概述

Set系列集合特点

  • 无序:存取顺序不一致
  • 不重复:可以去重复
  • 无索引:没有待索引的方法,所以不能使用普通的for循环遍历,也不能通过索引来获取元素

Set集合实现类特点

  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
  • TreeSet:排序、不重复、无索引

HashSet元素无序的底层原理:哈希表

  • HashSet集合底层采取哈希表存储的数据
  • 哈希表是一种对于增删改查数据性能都较好的结构

哈希表的组成

  • JDK8之前的,底层使用数组+链表组成
  • JDK8之后的,底层采用数组+链表+红黑树组成
  • 哈希值:
    • 是JDK根据对象的地址,按照某种规则算出来的int类型的数值
  • public int hashCode():返回对象的哈希值
  • 对象哈希值的特点:
    • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
    • 默认情况下,不同对象的哈希值是不同的

1673147613871.png

1673147803760.png

1673148047524.png

HashSet元素去重复的底层原理

package com.wuxibin.collection;

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

public class SetDemo {
    public static void main(String[] args) {
        Set<Student> sets = new HashSet<>();

        Student s1 = new Student("张三", 20, "男");
        Student s2 = new Student("张三", 20, "男");
        Student s3 = new Student("李四", 20, "女");

        sets.add(s1);
        sets.add(s2);
        sets.add(s3);

        System.out.println(sets);
    }
}


package com.wuxibin.collection;

import java.util.Objects;

public class Student {
    private String name;
    private int age;
    private String sex;

    public Student(){
    }

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

    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 getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @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) &&
                Objects.equals(sex, student.sex);
    }

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

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

实现类:LinkedHashSet

  • 有序、不重复、无索引
  • 有序指的是保证存储和取出的元素顺序一样
  • 原理:底层数据结构依然是哈希表,只是每个元素又额外多了一个双链表的机制记录存储顺序

实现类:TreeSet

  • 可排序、不重复、无索引
  • 可排序:按照元素的大小默认升序(由小到大)排序
  • TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
  • 注意:TreeSet集合是一定要排序的,可以将元素按照指定的规则进行排序

TreeSet集合默认的规则

  • 对于数值类型:Integer,Double,官方默认按照大小进行升序排序
  • 对于字符串类型:默认按照首字符的编号升序排序
  • 对于自定义类型如Student对象,TreeSet无法直接排序
  • 结论:想要使用TreeSet存储自定义类型,需要制定排序规则

自定义排序规则:

方式一:

  • 让自定义类(如学生类)实现Comparable接口重写里面的compareTo方法来定制比较规则
public class Student implements Comparable<Student>{
    
    ...
        

	@Override
    public int compareTo(Student o){
     	 //按照年龄比较
        return this.age - o.age;
    }      
}

方式二:

  • TreeSet集合有参数构造器,可以设置Comparator接口对应的比较器对象,来定制比较规则
Set<Student> sets = new TreeSet<>(new Comparator<Student>{
    @Override
    public int compare(Student o1,Student o1){
        return o1.getage() - o2.age(); //升序
  //    return o2.getage() - o1.age(); //降序
  //    浮点型建议直接使用Double.compare进行比较
        return Double.compare(o1.getage(),o2.age());
        return Double.compare(o2.getage(),o1.age());    
    }
})
    
    
    
Set<Student> sets = new TreeSet<>(( o1,o2) -> Double.compare(o1.getage(),o2.age()))    

Collection体系的特点、使用场景总结

1673154098729.png

补充知识:可变参数

可变参数

  • 可变参数用在形参中可以接收多个数据
  • 格式:数据类型...参数名称

可变参数的作用:

  • 传输参数非常灵活,方便。可以不传输参数,可以传输1个或者多个,也可以传输一个数组
  • 可变参数在方法内部本质上就是一个数组

注意事项:

  • 一个形参列表中可变参数只能有一个
  • 可变参数必须放在形参列表的最后面

补充知识:集合工具类Collections

Collections集合工具类

  • java.util.Collections:是集合工具类
  • 作用:Collections并不属于集合,是用来操作集合的工具类

Collection常用的API

方法名称 说明
public static boolean addAll(Collection<? super T> c, T...elements) 给集合对象批量添加元素
public static void shuffle(List<?> list) 打乱List集合元素的顺序

Collection排序相关的API

  • 使用范围:只能对于List集合的排序

排序方式1:

方法说明 说明
public static void sort(List list) 将集合中的元素按照默认规则排序
  • 注意:本方式不可以直接对自定义类型的List集合排序,除非自定义类型实现比较规则Comparable接口

排序方式2:

方法名称 说明
public static void sort(List list,Comparator<? super T> c) 将集合中元素按照指定规则排序

Collection体系的综合案例

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class GarmDemo {

    private static List<Card> lists = new ArrayList<>();

    static{
        String[] sizes = {"3","4","5","6","7","8","9","10","J","Q","K"};
        String[] colors = {"♠","♥","♣","♦"};

        int index = 0;
        for (String size : sizes) {
            index ++;
            for (String color : colors) {
                Card c = new Card(size,color,index);
                lists.add(c);
            }
        }

        Card c1 = new Card("","小王",++index);
        Card c2 = new Card("","大王",++index);
        Collections.addAll(lists, c1,c2);

        System.out.println("新牌:"+lists);
    }

    public static void main(String[] args) {
        Collections.shuffle(lists);
        System.out.println("洗牌:"+lists);

        List<Card> zhangsan = new ArrayList<>();
        List<Card> lisi = new ArrayList<>();
        List<Card> wangmazi = new ArrayList<>();

        for (int i = 0; i < lists.size() - 3; i++) {
            Card c = lists.get(i);
            if (i % 3 == 0){
                zhangsan.add(c);
            }else if (i % 3 == 1){
                lisi.add(c);
            }else if (i % 3 == 2){
                wangmazi.add(c);
            }
        }
        List<Card> lastThreeCards = lists.subList(lists.size() - 3, lists.size());

        sortCards(zhangsan);
        sortCards(lisi);
        sortCards(wangmazi);

        System.out.println("张  三:"+zhangsan);
        System.out.println("李  四:"+lisi);
        System.out.println("王麻子:"+wangmazi);
        System.out.println("三张底牌:"+lastThreeCards);

    }

    private static void sortCards(List<Card> cards) {
        Collections.sort(cards, new Comparator<Card>() {
            @Override
            public int compare(Card o1, Card o2) {
                return o2.getIndex() - o1.getIndex();
            }
        });
    }
}


public class Card {
    private String size;
    private String color;
    private int index;

    public Card(){

    }

    public Card(String size, String color,int index) {
        this.size = size;
        this.color = color;
        this.index = index;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public String getSize() {
        return size;
    }

    public void setSize(String size) {
        this.size = size;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return color+size;
    }
}

Map集合体系

Map集合的概述

  • Map集合是一种双列集合,每个元素包含两个数据

  • Map集合的每个元素格式:key=value(键值对元素)

  • Map集合也被称为“键值对集合

  • 格式:

    Collection集合的格式:[元素1,元素2,元素3 ...]
    Map几个的完整格式:{key1=value1,key2=value2,key3=value3 ...}
    

Map集合体系特点

  • Map集合的特点都是由键决定的
  • Map集合的键是无序,不重复的,无索引的,值不做要求(可以重复)
  • Map集合后面重复的键对应的值会覆盖前面重复键的值
  • Map集合的键值对都可以为null

Map集合实现类特点:

  • HashMap:元素是无序,不重复,无索引,值不做要求(与Map体系一致)
  • LinkedHashMap:元素是有序,不重复,无索引,值不做要求
  • TreeMap:元素是可排序,不重复,无索引,值不做要求

Map集合常用API

方法名称 说明
V put(K key,V value) 添加元素
V remove(Object key) 根据键删除键值对元素
void clear 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValues(Object value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

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

   	Map<String, Integer> maps = new HashMap<>();
   	maps.put("iphone", 10);
   	maps.put("娃娃", 20);
   	maps.put("iphoneX", 10);
   	maps.put("华为", 100);
   	maps.put("生活用品", 10);
   	maps.put("手表", 10);

   	System.out.println(maps);

   	// 清空集合
   	
   	 maps.clear(); System.out.println(maps);
   	 
   	// 判断集合是否为空,为空返回true,反之

   	System.out.println(maps.isEmpty());

   	// 根据键获取对应值:public V get(Object key)

   	Integer key = maps.get("华为");
   	System.out.println(key);
   	System.out.println(maps.get("生活用品"));
   	System.out.println(maps.get("生活用品2"));

   	// 根据键删除整个元素

   	maps.remove("iphone");
   	System.out.println(maps);

   	// 判断是否包含某个键,包含返回true,反之

   	System.out.println(maps.containsKey("iphone"));
   	System.out.println(maps.containsKey("娃娃"));

   	// 判断是否包含某个值

   	System.out.println(maps.containsValue(99));

   	// 获取全部键的集合

   	Set<String> keys = maps.keySet();
   	System.out.println(keys);

   	// 获取全部值得集合

   	Collection<Integer> values = maps.values();
   	System.out.println(values);

   	// 集合的大小

   	System.out.println(maps.size());

   	// 合并其他Map集合

   	Map<String, Integer> map1 = new HashMap<>();
   	map1.put("java1", 1);
   	map1.put("java2", 100);
   	Map<String, Integer> map2 = new HashMap<>();
   	map2.put("java3", 1);
   	map2.put("java4", 100);

   	map1.putAll(map2);
   	System.out.println(map1);
   	System.out.println(map2);
   }
}

Map集合的遍历方式一:键找值

  1. 先获取Map集合的全部建的Set集合

  2. 遍历键的Set集合,然后通过键提取对应值

  3. Set<K> keySet()               获取所有键的集合
    V get(Object key)             根据键获取值
    
		Set<String> keys = maps.keySet();
		for(String key : keys) {
			int value = maps.get(key);
			System.out.println(key + "===>"+ value);
		};

Map集合的遍历方式二:键值对

  • 先把Map集合转换成Set集合,Set集合中每个元素都是键值对实体类型

  • 遍历Set集合,然后提取键以及提取值

    Set<Map.Entry<K,V>>entrySet()           获取所有键值对对象的集合
    K getKey()                              获取键
    K getValue()                            获取值
    
		Set<Map.Entry<String, Integer>> entries = maps.entrySet();
		for(Map.Entry<String, Integer> entry : entries) {
			String key = entry.getKey();
			Integer value = entry.getValue();
			System.out.println(key +"===>"+ value);
		}

Map集合的遍历方式三:lambda表达式

maps.forEach((k,v)->{
    System.out.println(key +"--->"+value);
});
/*
   	 * maps.forEach(new BiConsumer<String,Integer>(){
   	 * 
   	 * @Override public void accept(String key, Integer value) { 
   	 * System.out.println(key +"--->"+value); } });
   	 */

   	maps.forEach((key,value) -> System.out.println(key +"--->"+value));
   	

Map集合案例

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class MapTest {
	public static void main(String[] args) {
		//1.把80个学生选择的数据拿出来
		String[] selects = {"A","B","C","D"};
		StringBuilder sb = new StringBuilder();
		Random r = new Random();
		for(int i=0; i < 80 ;i++) {
			sb.append(selects[r.nextInt(selects.length)]);
		}
		System.out.println(sb);
			
		Map<Character,Integer> infos = new HashMap<>();
		
		for (int i = 0; i < sb.length(); i++) {
			char ch = sb.charAt(i);
			
			if(infos.containsKey(ch)) {
				infos.put(ch, infos.get(ch)+1);
			}else {
				infos.put(ch, 1);
			}
		}
		System.out.println(infos);
	}
}

Map集合的实现类HashMap

1673241339770.png

HashMap的特点

  • HashMap是Map里面的一个实现类。特点都是由键决定的:无序、不重复、无索引

  • 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了

  • HashMap跟HashSet底层原理一模一样,都是哈希表,知识HashMap的每个元素包含两个元素而已

  • 实际上:Set底层集合就是Map实现的,只是Set集合中的元素只要键数据,不要值而已

  • public HashSet(){
        map = new HashMap<>();
    }
    

Map集合的实现类LinkedHashMap

LinkedHashMap集合概述和特点

  • 由键决定:有序、不重复、无索引
  • 这里的有序指的是保证存储和取出的元素顺序一致
  • 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录的顺序

Map集合的实现类TreeMap

  • 由键决定:不重复、无索引、可排序
  • 可排序:按照键的大小默认升序(由小到大)排序。只能对键排序
  • 注意:TreeMap集合是一定要排序的,可以默认排序,也可以将键按照指定的规则排序
  • TreeMap跟TreeSet一样底层原理是一样的

TreeMap集合自定义排序的两种方式

  1. 类实现Coparable接口,重写比较规则
  2. 集合自定义Comparator比较器对象,重写比较规则

补充知识:集合的嵌套

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CollectionMapDemo {
	public static void main(String[] args) {
		Map<String,List<String>> data = new HashMap<>();
		
		List<String> list1 = new ArrayList<>();
		Collections.addAll(list1, "A","C","D");
		data.put("张三", list1);

		List<String> list2 = new ArrayList<>();
		Collections.addAll(list2, "C","D");
		data.put("李四", list2);
		
		List<String> list3 = new ArrayList<>();
		Collections.addAll(list3, "A","B","C","D");
		data.put("王麻子", list3);
		
		
		Map<String,Integer> maps = new HashMap<>();
		
		Collection<List<String>> values = data.values();
		
		for(List<String> value : values) {
			for(String s : value) {
				if(maps.containsKey(s)) {
					maps.put(s,maps.get(s)+1);
				}else
				  maps.put(s,1);
			}
		}
		System.out.println(maps);
	}

}

创建不可变集合

  • 在List、Set、Map接口中,都存在of方法,可以创建一个不可变的集合
方法名称 说明
static List of(E...elements) 创建一个具有指定元素的List集合
static Set of(E...elements) 创建一个具有指定元素的Set集合
static <K,V> Map<K,V> of(E...elements) 创建一个具有指定元素的Map集合

Stream流

Stream流的概述

  • 在Java 8 中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream流概念
  • 目的:用于简化集合和数组操作的API

Stream流的获取

  1. 获取Stream流: 创建一条流水线,并把数据放在流水线上准备进行操作
  2. 中间方法: 流水线上的操作。一次操作完毕后,还可以继续进行其他操作
  3. 终结方法: 一个Steam流只能有一个终结方法,是流水线上的最后一个操作
  • 获取方式: 可以使用Collection接口中的默认方法stream()生成流

  • public static <T> Stream<T> stream(T[] array)   获取当前数组的Stream流
    public static <T> Stream<T> of(T... values)   获取当前数组的Stream流
    
public class CollectionMapDemo {
	public static void main(String[] args) {
//		Collection集合获取流
		Collection<String> list = new ArrayList<>();
		Stream<String> s =list.stream();
		
//		Map集合获取流
		Map<String,Integer> maps = new HashMap<>();
		//  键流
		Stream<String> keyStream = maps.keySet().stream();
		//  值流
		Stream<Integer> valueStream = maps.values().stream();
		//  键值对流
		Stream<Map.Entry<String, Integer>> keyValueStream = maps.entrySet().stream();
		
//		数组获取集合流
		String[] names = {"张三","李四","王麻子"};
		Stream<String> nameStream = Arrays.stream(names);
		Stream<String> nameStream2 = Stream.of(names);		
	}
}

Stream流的常用的API

  • 中间方法:
名称 说明
Stream filter(Predicate<? super T> predicate) 用于对流中的数据进行过滤
Stream limit(long maxSize) 获取前几个元素
Stream skip(long n) 跳过前几个元素
Stream distinct() 去除流中重复的元素。依赖(hashCode和equals方法)
static Stream concat(Stream a,Stream b) 合并a和b两个流为一流
  • 终结方法
名称 说明
void forEach(Consumer actor) 对此流的每个元素执行遍历操作
long count() 返回此流中的元素数

收集Stream流

  • 就是把Stream流操作后的结果数据转回到集合或者数组中去

    1673329724191.png

异常处理

异常概述、体系

  • 异常是程序在“编译”或者“运行”的过程中可能出现的问题
  • 比如:数组索引越界、空指针异常、日期格式化异常等等
  • 异常一旦出现了,如果没有提前处理,程序就会退出JVM虚拟机二终止

异常体系:

1673340360170.png

1673340524072.png

常见运行时的异常

  • 直接继承自RuntimeException或者其子类。编译阶段不会报错,运行时可能出现的错误
  • 示例:
    • 数组索引越界异常:ArraylndexOutOfBoundsExcecption
    • 空指针异常:NullPointerException,直接输出没有问题,但是调用空指针的变量的功能就会报错
    • 数学操作异常:ArithmenticException
    • 类型转换异常:ClassCastException
    • 数字转换异常:NumberFormatException
  • 运行时异常:一般是程序员业务没有考虑好或者是编程逻辑不严谨引起的程序错误

常见编译时的异常

  • 不是RuntimeException或者其子类的异常,编译阶就报错,必须处理,否则代码不通过

    public class Main{
        public static void main(String[] args) throws ParseException {
            String date = "2015-01-12 10:23:21";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date d = sdf.parse(date);  //日期解析日常:ParseException
            System.out.println(d);
        }
    }
    

异常的默认处理流程

  1. 默认会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException
  2. 异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM虚拟机
  3. 虚拟机接收到异常对象后,先在控制台直接输出异常栈信息数据
  4. 直接从当前执行的异常点干掉当前程序
  5. 后续代码没有机会执行了,因为程序已经死亡

编译时异常的处理机制

  • 出现异常直接抛出给调用者,调用者也继续抛出去
  • 出现异常自己捕获处理,不麻烦别人
  • 前两者结合,出现异常直接抛出去给调用者,调用者捕获处理

异常处理方式1:throws

  • throws:用在方法上,可以将方法内部出现的异常抛出去给本方法的调用者处理

  • 这种方法并不好,发生异常的方法自己不处理异常,如果异常最终抛出去给虚拟机将引起程序死亡

    方法 throws 异常1,异常2,异常3...{
        
    }
    
    规范做法:
    方法 throws Exception{
    
    }
    

异常处理方式2:try...catch...:

  • 监视捕获异常,用在方法内部,可以将方法内部出现的异常直接捕获处理

  • 这种方式还可以,发生异常的方法自己独立完成异常的处理,程序可以继续往下执行

    格式:
    try{
    //监视可能出现异常的代码
    }catch{
    //处理异常
    }catch{
    //处理异常
    }...
        
    建议格式:
    try{
    	//可能出现异常的代码
    }catch(Exception e){
    	e.printStackTeace(); //直接打印异常栈信息
    }
    
    Exception可以捕获处理一切异常类型
    

异常处理3:前两者结合

  • 方法直接将异常通过throws抛出去给调用者
  • 调用者收到异常后直接捕获处理

运行时异常的处理机制

  • 运行时异常编译阶段不会出错,是运行才可能出错的,所以编译阶段不处理也可以
  • 按照规范建议还是处理:建议在最外层调用处集中捕获处理即可

异常处理使代码更稳健的案例

public class Main{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        while (true){
            try {
                System.out.println("请输入合理的价格:");
                String str = sc.nextLine();
                double price = Double.valueOf(str);
                if (price > 0){
                    System.out.println("定价:"+price);
                    break;
                }else {
                    System.out.println("价格输入不合适,请重新输入~~");
                }
            } catch (NumberFormatException e) {
                System.out.println("价格输入不合适,请重新输入~~");
            }
        }
    }
}

自定义异常

  • Java无法为这个世界上全部的问题提供异常类。
  • 如果企业想通过异常的方式来管理自己的某个业务问题,就需要自定义异常类了。
  • 可以使用异常机制管理业务问题,如提醒程序员注意
  • 作用:编译时异常是编译阶段就会报错,提醒更加强烈,一定需要处理!
  • 同时一旦出现bug会自动提醒

自定义异常的分类:

  1. 自定义编译时异常:
    • 定义一个异常类继承Exception
    • 重写构造器
    • 在出现异常的地方用throws new 自定义对象抛出
public class Main{
    public static void main(String[] args) {
        try {
            checkAge(-34);
        } catch (ageIllegalException e) {
            e.printStackTrace();
        }
    }

    public static void checkAge(int age) throws ageIllegalException {
        if (age <0 || age > 200){
            //抛出异常对象给调用者
            //throw  :在方法内部直接创建一个异常,从此点抛出
            //throws :用在方法声明上的,抛出方法内部的异常
            throw new ageIllegalException(age + " is illegal");
        }else {
            System.out.println("年龄合法");
        }
    }
}



public class ageIllegalException extends Exception{
    public ageIllegalException() {
    }

    public ageIllegalException(String message) {
        super(message);
    }
}
  1. 自定义运行异常:
    • 定义一个异常类RuntimeException
    • 重写构造器
    • 在出现异常的地方用throw new自定义对象抛出
    • 作用:提醒不强烈,编译阶段不报错!!运行时才可能出现
public class Main{
    public static void main(String[] args) {

        try {
            checkAge(-34);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void checkAge(int age) {
        if (age <0 || age > 200){
            //抛出异常对象给调用者
            //throw  :在方法内部直接创建一个异常,从此点抛出
            //throws :用在方法声明上的,抛出方法内部的异常
            throw new ageIllegalRuntimeException(age + " is illegal");
        }else {
            System.out.println("年龄合法");
        }
    }
}


public class ageIllegalRuntimeException extends RuntimeException {
    public ageIllegalRuntimeException() {
    }
    
    public ageIllegalRuntimeException(String message) {
        super(message);
    }
} 

日志框架

日志技术的概述

  • 生活中的日志:生活中的日志就好比日记,可以记录你生活的点点滴滴
  • 程序中的日志:程序中的日志可以用来记录程序运行过程中的信息,并可以进行永久存储

日志技术具备的优势:

  • 可以将系统执行的信息选择性的记录到指定的位置(控制台、文件中、数据库中)
  • 可以随时以开关的形式控制是否记录日志,无需修改源代码

日志技术体系结构

  • 日志规范:一些接口,提供给日志的实现框架设计的标准
  • 日志框架:牛人或者第三方公司已经做好的日志记录实现代码,后来者直接可以拿去使用
  • 因为Commons Logging的接口不满意,有人就搞了SLF4J。因为对Log4j的性能不满意有人就搞了Logback

1673425384931.png

Logback概述

Logback主要分为三个技术模块:

  1. logback-core : logback-core 模块为其他两个模块奠定了基础,必须有。
  2. logback-classic: 它是log4j的一个改良版本,同时它完整实现了slf4j API。
  3. logback-access 模块与Tomcat和jetty 等Servlet容器集成,以提供HTTP访问日志功能

Logback快速入门

1673428690755.png

Logback配置详解-输出位置、格式设置

  • Logback日志文件的特性都是通过核心配置文件Logback.xml控制的

  • Logback日志文件输出位置、格式设置:

    • 通过logback.xml中的标签可以设置输出位置和日志文件信息的详细格式
    • 通常可以设置2个日志输出位置:一个是控制台,一个是系统文件中
  • 输出到控制台的配置标志:

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    
  • 输出到系统文件的配置标语:

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    

Logback配置详解-日志级别输出

  • 日志文件:
    • 级别程度依次是:TRACE<DEBUG<INFO<WARN<ERROR;默认级别是debug(忽略大小写),对应其方法
    • 作用:用于控制系统中哪些日志级别是可以输出的,只输出级别不低于设定级别的日志信息
    • ALL和OFF分别是打开全部日志信息,及关闭全部日志信息
  • 具体在标签的level属性中设置日志级别

File 类概述

  • File类在包java.io.File下、代表操作系统的文件对象(文件、文件夹)
  • File类提供了诸如:定位文件,获取文件本身的信息、删除文件、创建文件(文件夹)等功能。
方法名称 说明
public File(String pathname) 根据文件路径创建文件对象
public File(String parent,String child) 从父路径名字字符串和子路径名字字符串创建文件对象
public File(File parent,String child) 根据父类路径对应文件对象和子路径名字字符串创建文件对象
  • File对象可以定位文件和文件夹
  • File封装的对象仅仅是一个路径名,这个路径可以是存在的,也可以是不存在的

绝对路径:从盘符开始

File f = new File("D:\\code\\a.txt")

相对路径:不带盘符,默认直接到当前工程下的目录寻找文件

File f = new File("模块名\\a.txt")

File类的常用API

判断文件类型、获取文件信息

1673591915879.png

创建文件、删除功能

1673592733036.png

遍历文件夹

1673593585550.png

方法递归

递归的形式和特点

什么是方法递归?

  • 方法直接调用自己或者间接调用自己的形式称为方法递归(recursion)
  • 递归做为一种算法在程序设计语言中广泛应用

递归的形式

  • 直接递归:方法自己调用自己
  • 间接递归:方法调用其他方法,而其他方法又回调方法自己

方法递归存在的问题?

  • 递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出现象

递归的算法流程、核心要素

递归解决问题思路

  • 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来解决

递归算法的三要素大体可以总结为:

  1. 递归的公式:f(n)=f(n-1)*n;
  2. 递归的终点:f(1);
  3. 递归的方向必须走向终结点;

递归常见案例

public static int f(int n){
    if(n == 1){
        return 1;
    }else{
        return f(n-1)*n;
    }
}

//猴子吃桃

非规律化递归案例-文件搜索

public class Test01 {
    public static void main(String[] args) {
        searchFile(new File("E:/"),"Typora.exe");
    }

    private static void searchFile(File dir,String fileName) {
        if (dir != null && dir.isDirectory()){
            File[] files = dir.listFiles();
            if (files != null && files.length >0){
                for (File file : files) {
                    if (file .isFile()){
                        //是文件
                        if (file.getName().contains(fileName)){
                            System.out.println("找到了"+file.getAbsolutePath());
                        }
                    }else {
                        //是文件夹
                        searchFile(file, fileName);
                    }
                }
            }
        }else {
            System.out.println("对不起,当前搜索的位置不是文件夹");
        }
    }
}

非规律化递归案例-啤酒问题

public class Test01 {

    public static int totalNumber;//总数量
    public static int lastBottleNumber;//每次剩余的瓶子数
    public static int lastCoverNumber;//每次剩余的盖子数

    public static void main(String[] args) {
        buy(10);
        System.out.println("总数:"+totalNumber);
        System.out.println("剩余的瓶子数:"+lastBottleNumber);
        System.out.println("剩余的盖子数:"+lastCoverNumber);
    }

    private static void buy(int money) {
        int buyNumber = money / 2;
        totalNumber += buyNumber;

        int coveNumber = lastCoverNumber + buyNumber;
        int bottleNumber = lastBottleNumber + buyNumber;

        int allMoney = 0;
        if (coveNumber >= 4){
            allMoney += (coveNumber / 4) * 2;
        }
        lastCoverNumber = coveNumber % 4;

        if (bottleNumber >= 2){
            allMoney += (bottleNumber / 2) * 2;
        }
        lastBottleNumber = bottleNumber % 2;

        if (allMoney >= 2){
            buy(allMoney);
        }
    }
}

字符集

常见字符集介绍

  • 计算机底层不可以直接存储字符的。计算机中底层只能存储二进制(0,1)
  • 二进制是可以转成十进制的
  • 结论:计算机底层可以表示十进制编号。计算机可以给人类字符进行编号存储,这套编号规则就是字符集

ASCII字符集:

  • ASCII:包括了数字、英文、符号
  • ASCII使用1个字节存储一个字符,一个字节是8位,总共可以表示128个字符信息,对于英文,数字来说是够用的
01100001     97     a
01100010     98     b

GBK:

  • window系统默认的码表。兼容ASCII码表,也包含了几万个汉字,并支持繁体汉字以及部分日韩文字
  • 注意:GBK是中国的码表,一个中文以两个字节的形式存储,但不包含世界上所有国家的文字

Unicode:

  • unicode(又称统一码、万国码、单一码)是计算机科学技术领域里的一项业界字符编码标准
  • 容纳世界上大多数的所有常见文字和符号
  • 由于Unicode会先通过UTF-8、UTF-16、以及UTF-32的编码成二进制后再存储到计算机中,其中最为常见的是UTF-8
  • 注意:
    1. Unicode是万国码,以UTF-8编码后一个中文一般以三个字节的形式存储
    2. UTF-8也要兼容ASCII编码表
    3. 技术人员都应该使用UTF-8的字符集编码
    4. 编码前和编码后的字符集需要一致,否则会出现中文乱码

字符集的编码、解码操作

String编码:

方法名称 说明
byte[] getBytes() 使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中
byte[] getBytes(String charsetName) 使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中

String解码:

构造器 说明
String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的String
String(byte[] bytes,String charssetName) 通过指定的字符集解码指定的字节数组来构造新的String

IO流概述

IO流也称为输入、输出流,就是用来读写数据的。

  • I表示intput,是数据从硬盘文件读入到内存的过程,称之为输入,负责读
  • O表示output,是内存程序的数据从内存写出到硬盘文件的过程,称之为输出,负责写

分类:

  • 按流的方向分:输入流 输出流
  • 按流中的数据最小单位分为:字节流 字符流:只能操作纯文本文件
  1. 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流称为字节输入流
  2. 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流称为字节输出 流
  3. 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流称为字节输入流
  4. 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络中去的流称为字节输出 流

1673665505102.png

字节流的使用

文件字节输入流:每次读取一个字节

  • FileIntputStream:
构造器 说明
public FileIntputStream(File file) 创建字节输入流管道与源文件对象接通
public FileIntputString(String pathname) 创建字节输入流管道与源文件路径接通
方法名称 说明
public int read() 每次读取一个字节返回,如果字节没有可读的返回-1
public int read(byte[] buffer) 每次读取一个字节数组返回,如果字节数组已经没有可读的返回-1
  • 性能较慢
  • 读取到中文字符无法避免乱码问题
import java.io.FileInputStream;
import java.io.InputStream;

public class Test {
    public static void main(String[] args) throws Exception{
        InputStream is = new FileInputStream("MethodDemo\\test.txt");
//        int b1 = is.read();
//        System.out.println(b1);
//
//        int b2 = is.read();
//        System.out.println(b2);
//
//        int b3 = is.read();
//        System.out.println(b3);
//
//        int b4 = is.read();
//        System.out.println(b4);
//
//        int b5 = is.read();
//        System.out.println(b5);

        int b ;
        while ((b = is.read()) != -1){
            System.out.print((char) b);
        }
    }
}

文件字节输入流:每次读取一个字节数组

import java.io.FileInputStream;
import java.io.InputStream;

public class Test {
    public static void main(String[] args) throws Exception{
        InputStream is = new FileInputStream("MethodDemo\\test.txt");

//        byte[] buffer = new byte[3];
//        int len = is.read(buffer);
//        System.out.println("读取了"+len+"个字节");
//        String str = new String(buffer,0,len);
//        System.out.println(str);

//        int len1 = is.read(buffer);
//        System.out.println("读取了"+len1+"个字节");
//        String str1 = new String(buffer);
//        System.out.println(str1);

        byte[] buffer = new byte[3];
        int len;
        while ((len = is.read(buffer)) != -1){
            String str = new String(buffer,0, len);
            System.out.print(str);
        }
    }
}

  • 读取的性能提升了
  • 无法避免中文字符输出乱码

文件字节输入流:一次读完全部字节

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class Test {
    public static void main(String[] args) throws Exception{
        File f = new File("MethodDemo\\test.txt");
        InputStream is = new FileInputStream(f);

        byte[] buffer = new byte[(int) f.length()];
        int len = is.read(buffer);
        System.out.println("读了多少个字节:"+len);
        System.out.println("文件大小:"+f.length());
        System.out.println(new String(buffer));
    }
}

1673753923514.png

1673753990818.png

文件字节输出流:写字节数据到文件

import java.io.*;

public class Test {
    public static void main(String[] args) throws Exception{
//        OutputStream os = new FileOutputStream("MethodDemo\\test02.txt");
        OutputStream os = new FileOutputStream("MethodDemo\\test02.txt",true);

        os.write('a');

        byte[] buffer = {'a',97,98,99};
        os.write(buffer);

        os.write("\r\n".getBytes());

        byte[] buffer1 = "我是中国人".getBytes();
        os.write(buffer1);

        os.write(buffer, 0, 2);

        os.flush();
        os.close();
    }
}

文件拷贝

import java.io.*;

public class Test {
    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\bc4d441429c683610ff0f7c182f7663.jpg");

            OutputStream os = new FileOutputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\1.jpg");

            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1){
                os.write(buffer,0,len);
            }

            System.out.println("复制成功了!");

            os.close();
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

资源释放的文件

try-catch-finally

import java.io.*;

public class Test {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\bc4d441429c683610ff0f7c182f7663.jpg");

            os = new FileOutputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\1.jpg");
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1){
                os.write(buffer,0,len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (os!=null)os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (is!=null)is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("复制成功了!");
    }
}

try-with-resource

import java.io.*;

public class Test {
    public static void main(String[] args) {
        try (
            InputStream is = new FileInputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\bc4d441429c683610ff0f7c182f7663.jpg");

            OutputStream os = new FileOutputStream("C:\\Users\\Pomelo\\Pictures\\证件照\\1.jpg");
            ){
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1){
                os.write(buffer,0,len);
            }
            System.out.println("复制成功了!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1673753436526.png

字符流的使用

文件字符输入流-一次读取一个字符

  • 文件字符输入流:Reader
  • 以内存为基准,把磁盘文件的数据以字符的形式读取到内存中去

1673754444617.png

文件字符输入流-一次读取一个字符数组

import java.io.*;

public class Test {
   public static void main(String[] args) throws Exception {
       Reader fr = new FileReader("MethodDemo\\test.txt");

       char[] buffer = new char[1024];
       int len ;
       while ((len = fr.read(buffer)) != -1){
           String str = new String(buffer, 0,len );
           System.out.println(str);
       }
   }
}

文件字符输出流

1673756289539.png

缓冲流

概述:

  • 缓冲流也称为高效流、或者高级流。之前学习的字节流也可称为原始流
  • 作用:缓冲流自带缓冲区、可以提高原始字节流、字符流读写数据的性能

字节缓冲流

  • bufferedInputStream 字节缓冲输入流

  • bufferedOutputStream 字节缓冲输出流

package com.wuxibin.file;

import java.io.*;

public class FileDemo {
    private static final String SRC_FILE = "D:\\微信下载\\WeChat Files\\wxid_3h4bhm9ie2ya22\\FileStorage\\Video\\2022-11\\1cb72795e1c6849a01d0465bfb1a4c63.mp4";
    private static final String DEST_FILE = "E:\\aaa\\";
    public static void main(String[] args) throws Exception {
        copy01();//低级字节流一个一个字节复制
        copy02();//低级字节流一个一个字节数组复制
        copy03();//缓冲字节流一个一个字节复制
        copy04();//缓冲字节流一个一个字节数组复制
    }

    private static void copy04() {
        long startTime = System.currentTimeMillis();
        try (
                InputStream is = new FileInputStream(SRC_FILE);
                InputStream bis = new BufferedInputStream(is);
                OutputStream os = new FileOutputStream(DEST_FILE+"video4.mp4");
                OutputStream bos = new BufferedOutputStream(os);
        ){
            int d;
            byte[] buffer = new byte[1024];
            while ((d = bis.read(buffer)) != -1){
                bos.write(d);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("缓冲字节流一个一个字节数组复制的速度是:"+(endTime-startTime)/1000.0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void copy03() {
        long startTime = System.currentTimeMillis();
        try (
                InputStream is = new FileInputStream(SRC_FILE);
                InputStream bis = new BufferedInputStream(is);
                OutputStream os = new FileOutputStream(DEST_FILE+"video3.mp4");
                OutputStream bos = new BufferedOutputStream(os);
        ){
            int d;
            while ((d = bis.read()) != -1){
                bos.write(d);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("缓冲字节流一个一个字节复制的速度是:"+(endTime-startTime)/1000.0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private static void copy02() {
        long startTime = System.currentTimeMillis();
        try (
                InputStream is = new FileInputStream(SRC_FILE);
                OutputStream os = new FileOutputStream(DEST_FILE+"video2.mp4");
        ){
            int d;
            byte[] buffer = new byte[1024];
            while ((d = is.read(buffer)) != -1){
                os.write(d);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("低级字节流一个一个字节数组复制的速度是:"+(endTime-startTime)/1000.0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public static void copy01(){
        long startTime = System.currentTimeMillis();
        try (
                InputStream is = new FileInputStream(SRC_FILE);
                OutputStream os = new FileOutputStream(DEST_FILE+"video1.mp4");
                ){
            int d;
            while ((d = is.read()) != -1){
                os.write(d);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("低级字节流一个一个字节复制的速度是:"+(endTime-startTime)/1000.0);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}


//E:\software\Java\jdk1.8.0_251\bin\java.exe "-javaagent:E:\software\IntelliJ 
低级字节流一个一个字节数组复制的速度是:0.155
缓冲字节流一个一个字节复制的速度是:0.266
缓冲字节流一个一个字节数组复制的速度是:0.02

字符缓冲流

  • bufferedReader 字符缓冲输入流

  • buffered Writer 字符缓冲输出流

package com.wuxibin.file;

import java.io.*;

public class FileDemo {
    public static void main(String[] args) throws IOException {
        try (
             Reader fr = new FileReader("file\\src\\test.txt");
             BufferedReader br = new BufferedReader(fr);
        ){

            String line;
            while (( line = br.readLine()) != null){
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

转换流

字符输入转换流

  • InputStreamRander 字符输入转换流:

    package com.wuxibin.file;
    
    import java.io.*;
    
    public class FileDemo {
        public static void main(String[] args) throws IOException {
                 InputStream is = new FileInputStream("file\\src\\test.txt");
                 Reader isr = new InputStreamReader(is,"GBK");
                 BufferedReader br = new BufferedReader(isr);
    
                String line;
                while (( line = br.readLine()) != null){
                    System.out.println(line);
                }
        }
    }
    
  • OutputStreamWriter 字符输出转换流

    //  "我爱你中国“.getByes(编码)
    
    OutputStream os = new OutputStream(" ");
    Writer osw = new OutputWriter(os,"编码");
    BufferedWriter bw = new BufferedWriter(osw);
    

序列化对象

对象序列化:

  • 作用:以内存为基准,把内存中的对象存储到磁盘文件中去,称为对象序列化
  • 使用到的流是对象字节输出流:ObjectOutputStream

1675412843380.png

对象如果要序列化,必须实现Serializable接口

transient修饰的成员变量不参与序列化接口

对象反序列化:

  • 使用到的流是对象字节输入流:ObjectInputStream
  • 作用:以内存为基准,把存储到磁盘文件中去的对象数据恢复成内存中的对象,称为对象的反序列化

打印流

1675414189327.png

打印流:

  • 作用:打印流可以实现方便、高效的打印数据到文件中去。打印流一般是指PrintStream、PrintWriter两个类
  • 可以实现打印什么数据就是什么数据,例如打印整数97写出去就是97,打印boolean的true写出去就是true

PrintStream

构造器 说明
public PrintStream(OutputStream os) 打印流直接通向字节输出管道流
public PrintStream(File f) 打印流会直接通向文件对象
public PrintStream(String filepath) 打印流直接通向文件路径
方法 说明
public void print(Xxx xx) 打印任意类型的数据出去

PrintWriter

构造器 说明
public PrintWriter(OutputStream os) 打印流直接通向字节输出管道流
public PrintWriter(File f) 打印流会直接通向文件对象
public PrintWriter(String filepath) 打印流直接通向文件路径
方法 说明
public void print(Xxx xx) 打印任意类型的数据出去

PrintStream和PrintWriter的区别:

  • 打印数据功能上是一模一样的,都是使用方便,性能高效(核心优势)
  • PrintStream继承自字节输出流OutputStream,支持写字节数据的方法
  • PrintWriter继承自字符输出流Writer,支持写字符数据出去
//重定向
PrintStream ps = new PrintStream("");
System.setOut(ps);

System.out.println("我爱你中国");

Properties

Properties属性集对象:

  • 其实就是一个Map集合,但是我们一般不会当集合使用,因为HashMap更好用

Properties核心作用:

  • Properties代表的是一个属性文件,可以吧自己对象中的键值对信息存入到一个属性文件中去
  • 属性文件:后缀是.properties结尾的文件,里面的内容都是key=value,后续做系统配置信息的

1675416709144.png

IO框架

commons-io概述

  • commons-io是Apache开源基金提供的一组有关IO操作的类库,可以提高IO功能开发的效率
  • commons-io工具包提供了很多有关IO操作的类,有两个主要的类FileUtils和IOUtils

FileUtils主要方法如下:

方法名 说明
String readFileToString(File file,String encoding) 读取文件中的数据,返回字符串
void copy(File srcFile,File destFile) 复制文件
void copyDirectoryToDirectory(File srcDir,File destDir) 复制文件夹

导入Commons-io-2.6.jar做开发

步骤:

  1. 在项目中创建一个文件夹:lib
  2. 将commons-io-2.6.jar文件复制到lib文件夹
  3. 在jar文件上点击右键,选择Add as Library —> 点击OK
  4. 在类中导包使用

线程

什么是线程:

  • 线程(thread)是一个程序内部的一条执行路径
  • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径
  • 程序中如果只有一条执行路径,那么这个程序就是单线程程序

多线程是什么?

  • 多线程是指从软硬件上实现多条执行流程的技术

多线程的创建

方式一:继承Thread类

  • java是通过java.lang.Thread类来代表线程的
  • 按照面向对象的思想,Thread类应该提供了实现多线程的方式
  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
package com.wuxibin.thread;

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new MyThread();
        //调用start方法启动线程(执行还是run方法)
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:"+i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:"+i);
        }
    }
}

优缺点:
  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展

方式二:实现Runnable接口

  1. 定义一个线程任务MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理
  4. 调用线程对象的start()方法启动线程
package com.wuxibin.thread;

public class ThreadDemo2 {
    public static void main(String[] args) {
        Runnable target = new MyRunnable();
        Thread t = new Thread(target);
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:"+i);
        }
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:"+i);
        }
    }
}
优缺点:
  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的
  1. 可以创建Runnable的匿名内部类对象
  2. 交给Thread处理
  3. 调用线程对象的start()启动线程
package com.wuxibin.thread;

public class ThreadDemo3 {
    public static void main(String[] args) {
        Runnable target = new MyRunnable(){
            @Override
            public void run(){
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程1输出:"+i);
                }
            }
        };
        Thread t1 = new Thread(target);
        t1.start();

        new Thread(new MyRunnable(){
            @Override
            public void run(){
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程2输出:"+i);
                }
            }
        }).start();


        new Thread(()->{
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程3输出:"+i);
                }
        }).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:"+i);
        }
    }
}

方式三:JDK5.0新增:实现Callable、FutureTask接口:

  1. 得到任务对象:
    • 定义类实现Callable接口、重写call方法、封装要做的事
    • 用FutureTask把Callable对象封装成线程对象
  2. 把线程任务对象交给Thread处理
  3. 调用Thread的start方法启动线程、执行任务
package com.wuxibin.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo4 {
    public static void main(String[] args) {
        //创建Callable任务对象
        Callable<String> call = new MyCallable(100);
        //FutureTask对象作用1是:是Runnable的对象(实现了Runnable接口)可以交给Thread
        //FutureTask对象作用2是:是线程执行结束完毕之后通过其调用get方法的到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call);
        Thread t = new Thread(f1);
        t.start();

        try {
            String str = f1.get();
            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程的执行结果是:"+sum;
    }
}
优缺点:
  • 优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强
  • 可以在线程执行完毕后去获取线程执行的结果
  • 缺点:编程复杂一点

1675578456231.png

Thead的常用方法

  • Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()
package com.wuxibin.thread;

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t1 = new MyThread("线程1");
        //调用start方法启动线程(执行还是run方法)
        t1.start();
        //t1.setName("线程1");
        System.out.println(t1.getName());

        Thread t2 = new MyThread("线程2");
        t2.start();
        //t2.setName("线程2");
        System.out.println(t2.getName());

        //哪个线程执行她,它就得到哪个线程对象(当前线程)
        Thread m = Thread.currentThread();
        System.out.println(m.getName());

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程输出:"+i);
        }
    }
}

package com.wuxibin.thread;

public class MyThread extends Thread{
    public MyThread() {
    }

    //为当前线程对象设置名称,送给父类有参数构造器初始化名称
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
        }
    }
}

Thread类的线程休眠方法:

public static void sleep(long time)  //让当前线程休眠指定时间后再继续执行,单位为毫秒
public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 5; i++) {
            System.out.println("输出:"+i);
            if (i == 3){
                Thread.sleep(3000);
            }
        }
    }
}

线程安全

线程安全问题是什么、发生的什么原因

  • 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题、称为线程安全问题
  • 原因:
    1. 存在多线程并发
    2. 同时访问共享资源
    3. 存在修改共享资源

线程同步

同步思想概述:

  • 为了解决线程安全问题
线程同步的核心思想:
  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来

方式一:同步代码块:

  • 作用:把出现线程安全问题的核心代码给上锁

  • 原理:每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来

  • synchronized(同步锁对象){
        操作共享资源的代码(核心代码)
    }
    

1675583129687.png

方式二:同步方法:

  • 作用:把出现线程安全问题的核心方法上锁

  • 原理:每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来

  • 修饰符 synchronized 返回值类型 方法名称(形参列表){
        操作共享资源的代码
    }
    

1675583444857.png

线程通信

1675590538383.png

线程池

线程池概述

  • 线程池就是一个可以复用线程的技术
  • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能

线程池实现的API、参数说明

  • 谁代表线程池:

    JDK5.0起提供了代表线程池的接口:ExecutorService

  • 如何得到线程池对象:

    • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

      ExecutorService   ->   ThreadPoolExecutor
      
    • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
参数 作用 限制
corePoolSize 指定线程池的线程数量(核心线程) 不能小于0
maximumPoolSize 指定线程池可支持的最大线程数 最大数量>=核心线程数量
keepAliveTime 指定临时线程的最大存活时间 不能小于0
unit 指定存活时间的点位(秒、分、时、天) 时间单位
workQueue 指定任务队列 不能为null
threadFactory 指定用哪个线程工厂创建线程 不能为null
handler 指定线程忙,任务满的时候,新任务来了怎么办 不能为null
临时线程什么时候创建:
  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
什么时候开始拒绝任务:
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来才会开始任务拒绝

线程池处理Runnable任务

  • ThreadPoolExecutor创建线程池对象实例:

    ExecutorService pools = new ThreadPoolExecutor(3,5,8,TimeUnit,SECONDS,new ArrayBlockingQueue<>(6),Executors.defaultThreadFactory(),new ThreadPoolExector.AbortPblicy());
    
  • ExecutorService的常用方法

    方法说明 说明
    void execute(Runnable command) 执行任务/命令,一般用来执行Runnable任务
    Futrue submit(Callbale task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
    void shutdown() 等任务执行完毕后关闭线程池
    List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

线程池处理Callable任务

  • 使用ExecutorService的方法
  • Future submit(Callable command)

Executors工具类实现线程池

1675657534449.png

1675657812122.png

定时器

  • 定时器是一种控制任务延时调用,或者周期调用的技术
  • 作用:闹钟、定时邮件发送

定时器的实现方法:

  • 方式一:Timer

1675852487582.png

  • 方式二:ScheduledExecutorService

1675861280848.png

补充知识:并发并行、生命周期

并发与并行:

  • 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的
并发的理解:
  • CPU同时处理线程的数量有限
  • CPU会轮询为系统每个线程服务,由于CPU切换的速度很快,给我们的感觉就是这些线程在同时执行,这就是并发
并行的理解:
  • 在同一时刻上,同时有多个线程在被CPU处理并执行

线程的生命周期

线程的状态:
  • 线程的状态:也就是线程从生到死的过程,以及中间经历各种状态及状态转换
  • 理解线程的状态有利于提升并发编程的理解能力
java线程的状态
  • java总共定义了6中状态

  • 6种状态都定义在Thread类的内部枚举类中

  • public class Thread{
        ...
            public enum State {
            	NEW,//新建状态
                RUNNABLE,//就绪状态
                BLOCKED,//阻塞状态
                WAITING,//等待状态
                TIMD_WAITING,//计时等待
                TERMINATED;//结束状态
        }
        ...
    }
    

1675862413334.png

网络编程

  • 网络编程可以让程序与网络上的其他设备中的程序进行数据交互
  • 常见的通信模式有如下2种形式:Client-Server(CS)、Browser/Server(BS)

网络通信的三要素

  • IP地址:设备在网络中的地址,是唯一的标识
  • 端口:应用程序在设备中唯一标识
  • 协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议

IP地址

  • IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标识
  • 常见的IP地址分类为;IPv4(4个字节)和IPv6
  • 1675864102653.png
IP地址形式:
  • 公网地址、和私有地址(局域网使用)
  • 192.168开头就是常见的局域网地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用
IP常用命令:
  • ipconfig:查看本机IP地址
  • ping IP :检查网络是否连接
特殊IP地址:
  • 本机IP:127.0.0.1或者localhost:称为回送地址也可称为本地回环地址,只会寻找当前所在本机
IP地址操作类-InetAddress

1675996439172.png

端口号:

  • 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535
  • 端口类型:
    • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
    • 注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占用8080,MySQL占用3306)
    • 动态端口:49152~65535,之所以它一般不固定分配某种进程,而是动态分配
  • 注意:我们自己开发的程序选择注册端口,是一个设备中不能出现两个程序的端口号一样,否则出错

协议

  • 通信协议
    • 连接和通信数据的规则被称为通信协议
    • 1675997040516.png
    • 1675998151533.png

UDP:

  • UDP是一种无连接、不可靠传输的协议
  • 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
  • 每个数据包的大小限制在64KB内
  • 发送不管对方是否准备好,接受方收到也不确认,故是不可靠的
  • 可以广播发送,发送数据结束时无需释放资源,开销小,速度快
  • UDP协议通信场景:语言通话,视频会话等

UDP通信

  • UDP是一种无连接、不可靠传输的协议
  • 将数据源IP、目的地IP和端口以及数据封装成数据包,大小在64KB内,直接发送出去即可

通信-快速入门

1676006482813.png

1676006535979.png

UDP通信-广播、组播

  • 单播:单台主机与单台主机之间的通信
  • 广播:当前主机与所在网络中的所有主机通信
  • 组播:当前主机与选定的一组主机的通信

TCP通信-快速入门

  • TCP是一种面向连接,安全,可靠的传输数据的协议
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
  • 在连接中可进行数据量的传输

1676007183563.png

编写客户端代码:

  1. 创建客户端Socket对象,请求与服务器的连接
  2. 使用Socket对象调用getOutputStream()方法得到字节输出流
  3. 使用字节输出流完成数据发送
  4. 关闭资源

编写服务端代码、原理分析

1676007521553.png

TCP通信-多发多收消息

  1. 可以使用死循环控制服务端收完消息继续等待接受下一个消息
  2. 客户端也可以使用死循环等待用户不断输入消息
  3. 客户端一旦输入了exit,则关闭客户端程序,并释放资源

单元测试:

单元测试概述

  • 单元测试就是针对最小功能单元编写测试代码,Java程序最小的功能单元是方法,因此,单元测试就是针对Java方法的测试,进而检查方法的正确性
Junit单元测试框架
  • Junit是Java语言实现的单元测试框架,它是开源的,Java开发者都应该学习并使用JUnit编写单元测试
  • 此外,几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写JUnit测试
  • 优点:
    1. JUnit可以灵活的选择执行哪些测试方法,可以一键执行全部测试方法
    2. JUnit可以生成全部方法的测试报告
    3. 单元测试中的某个方法测试失败了,不会影响其他测试方法的测试

单元测试快速入门:

  1. 将Junit的jar包导入到项目中
    • IDEA通常整合好了Junit框架,一般不需要导入
    • 如果IDEA没有整合好,需要自己手工导入如下2个JUnit包到模块
  2. 编写测试方法:该测试方法必须是公共的无参数无返回值的非静态方法
  3. 在测试方法上使用@Test注解:标注该方法是一个测试方法
  4. 在测试方法中完成测试方法的预期正确性测试
  5. 选中测试方法,选择"Junit运行",如果测试良好则是绿色;如果测试失败,则是红色

单元测试常用注解:

1676009492042.png