Java入门笔记补充

news2024/12/28 14:53:55

Java基础笔记补充

  • 数据类型
    • 数据类型
    • 整形拓展
    • 浮点型拓展
    • 字符型拓展
    • 类型转换
  • 变量
    • 变量作用域
      • 局部变量
      • 实例变量
      • 静态变量
    • 变量的命名规范
  • 数组
    • 三种初始化
      • 静态初始化
      • 动态初始化
      • 数组的默认初始化
    • Arrays类
  • 内存分析
    • 栈 stack:
    • 堆 heap:
    • 方法区(也是堆):
  • 封装继承多态
    • 继承
      • super this
      • override
    • 多态
    • 引用类型转换
  • 修饰符
    • static
      • 1、创建和初始化对象的过程
    • final
        • 1、修饰类
        • 2、修饰方法
        • 3、修饰变量
    • abstract
  • 接口
    • 接口与抽象类的区别
    • 接口中的方法都是抽象方法
    • 接口中的变量都是静态常量(public static final修饰)
    • 总结
  • 异常
    • 异常体系结构
    • 异常之间的区别与联系
      • 1、Error
      • 2、Exception
      • 3、检查异常和不受检查异常
  • 集合
    • 重中之重
    • List
      • ArrayList
        • 1、ArrayList概述
        • 2、ArrayList的数据结构
        • 3、ArrayList源码分析
          • 1、继承和层次关系
          • 2、类中的属性
          • 3、构造方法
          • 4、核心方法-add
          • 总结
        • 4、总结
      • LinkedList
        • 1、LinkedList概述
        • 2、LinkedList的数据结构
        • 3、LinkedList的特性
        • 4、LinkedList 源码分析
          • 类的属性
          • 构造方法
          • 内部类(Node)
          • 4、总结
      • Vector和Stack
        • Vector
          • 1、Vector概述
        • Stack
        • 总结
      • List总结
        • 【arrayList和LinkedList区别】
        • 【arrayList和Vector的区别】
        • 【fail-fast和fail-safe区别和什么情况下会发生】
        • 【举例说明fail-fast和fail-safe的区别】
        • 【为什么现在都不提倡使用vector了】
    • Map
      • HashMap
        • 1. HashMap数据结构
          • 1.1 HashMap概述
          • 1.2 HashMap在JDK1.8以前数据结构和存储原理
          • 1.3 JDK1.8后HashMap的数据结构
          • 1.4 HashMap的属性
        • 2. HashMap的源码分析
          • 2.1 HashMap的层次关系与继承结构
          • 2.2HashMap类的属性
          • 2.3 HashMap的构造方法
        • 3. 常用方法
        • 4. 总结
    • 迭代器
      • 问题:何遍历Map集合呢?
    • 泛型
    • Collections工具类
      • 2、排序操作
      • 3、查找、替换操作
      • 4、同步控制
  • IO
    • 1. 输入输出流分类
    • 2. 节点流和处理流
      • 2.1.节点流类型
      • 2.2.处理流类型
    • 3. InputStream(输入流)
      • 3.1.InputStream的基本方法
      • 3.2 案例
    • 4. OutputStream(输出流)
      • 4.1.OutputStream的基本方法
      • 4.2 案例
    • 5. Reader流
      • 5.1.Reader的基本方法
    • 6. Writer流
      • 6.1.Writer的基本方法
      • 6.2 演示
    • 7. 处理流讲解
      • 7.1.第一种处理流——缓冲流(Buffering)
      • 7.2.第二种处理流——转换流
      • 7.3 第三种处理流——数据流
      • 7.4.打印流——Print
      • 7.5. 对象流——Object
    • 8.IO流总结
  • 多线程
    • 线程简介
    • 线程创建(重点)
      • 1. 继承Thread类(重点)
        • 例子:多线程图片下载器
      • 2. 实现Runnable接口(重点)
        • 多线程抢票问题
        • 案例:龟兔赛跑
      • 3. 实现Callable接口(了解)
    • 静态代理模式
    • Lambda表达式
    • 线程状态
    • 线程方法
      • 线程停止
      • 线程睡眠 sleep
      • 线程礼让 yield
      • Join
      • 观测线程状态
      • 线程优先级
      • 守护线程
    • 线程同步(重点)
      • 三大不安全案例
        • 买票
        • 银行取钱
        • 不安全的集合
      • 同步方法
        • 弊端
      • 同步块
      • 不安全案例解决
        • 买票
        • 银行取钱
        • 集合
        • JUC集合
      • 死锁
        • 例子
        • 解决
      • Lock(锁)
        • synchronized和lock对比
    • 线程通信
      • 生产者消费者问题
      • 解决方法1 管程法
      • 解决方法2 信号灯
    • 线程池
    • 总结
  • 网络编程
    • 1. 网络编程概述
      • 1.1、概述
      • 1.2、网络通信两个要素
      • 1.3、IP
      • 1.4、端口号
      • 1.5、网络通信协议
        • TCP/IP协议簇
        • TCP 和 UDP对比
        • UDP协议
    • 2、TCP网络编程
      • 2.1、案例一
      • 2.2、案例二
      • 2.3、练习作业
      • 2.4、 初识Tomcat服务器
    • 3、UDP网络编程
      • 3.1、说明
      • 3.2、案例一
      • 3.3、案例二:在线咨询

数据类型

数据类型

image-20221213232419639

image-20221213232438263

整形拓展

二进制整数,如 : 0b0101(JDK1.7)

十进制整数,如:99, -500, 0。
八进制整数,要求以 0 开头,如:015。
十六进制数,要求 0x 或 0X 开头,如:0x15 。

浮点型拓展

别使用浮点数比较,要用的话用Decimal类

【金融面试问:银行金融业务用什么类型表示?】
浮点类型float, double的数据不适合在不容许舍入误差的金融计算领域。
如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal类。

主要理由:
由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。浮点数一般都存在舍入误差,很
多数字无法精确表示,其结果只能是接近,但不等于;二进制浮点数不能精确的表示0.1,0.01,0.001这样10的负次幂。并不是所有的小数都能可以精确的用二进制浮点数表示。
最好完全避免使用浮点数比较 !

大数值:Java.math下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数
值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。

浮点数使用总结:

  1. 默认是double
  2. 浮点数存在舍入误差,很多数字不能精确表示。如果需要进行不产生舍入误差的精确数字计算,需
    要使用BigDecimal类。
  3. 避免比较中使用浮点数

字符型拓展

  • ASCIIS码:
    1个英文字母(不分大小写)= 1个字节的空间
    1个中文汉字 = 2个字节的空间
    1个ASCII码 = 一个字节

  • UTF-8编码:

    英文 = 1个字节
    中文 = 3个字节

  • Unicode编码:
    英文 = 2个字节
    中文 = 2个字节

类型转换

浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入,例如:

(int)23.7 == 23;
(int)-45.89f == -45

变量

变量作用域

变量根据作用域可划分为三种:

  • 类变量(静态变量: static variable):独立于方法之外的变量,用 static 修饰。
  • 实例变量(成员变量:member variable):独立于方法之外的变量,不过没有 static 修饰。
  • 局部变量(local variable):类的方法中的变量。

局部变量

在使用前必须先声明和初始化(赋初值)。
局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。

实例变量

自动初始化成该类型的默认初始值

静态变量

自动初始化成该类型的默认初始值

变量的命名规范

  1. 所有变量、方法、类名:见名知意
  2. 类成员变量:首字母小写和驼峰原则 : monthSalary
  3. 局部变量:首字母小写和驼峰原则
  4. 常量:大写字母和下划线:MAX_VALUE
  5. 类名:首字母大写和驼峰原则: Man, GoodMan
  6. 方法名:首字母小写和驼峰原则: run(), runRun()

数组

三种初始化

静态初始化

除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。

int[] a = {1,2,3};
Man[] mans = {new Man(1,1),new Man(2,2)};

动态初始化

数组定义、为数组元素分配空间、赋值的操作、分开进行。

int[] a = new int[2];
a[0]=1;
a[1]=2;

数组的默认初始化

数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。

public static void main(String[] args) {
    int[] a=new int[2];
    boolean[] b = new boolean[2];
    String[] s = new String[2];
    System.out.println(a[0]+":"+a[1]); //0,0
    System.out.println(b[0]+":"+b[1]); //false,false
    System.out.println(s[0]+":"+s[1]); //null, null
}

Arrays类

具有以下常用功能:

  • 给数组赋值:通过 fill 方法。
  • 对数组排序:通过 sort 方法,按升序。
  • 比较数组:通过 equals 方法比较数组中元素值是否相等。
  • 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。

image-20221214002632216

1、打印数组

public static void main(String[] args) {
    int[] a = {1,2};
    System.out.println(a); //[I@1b6d3586
    System.out.println(Arrays.toString(a)); //[1, 2]
}

2、数组排序

public static void main(String[] args) {
    int[] a = {1,2,323,23,543,12,59};
    System.out.println(Arrays.toString(a));
    Arrays.sort(a);
    System.out.println(Arrays.toString(a));
}

3、二分法查找

在数组中查找指定元素并返回其下标
注意:使用二分搜索法来搜索指定的数组,以获得指定的值。必须在进行此调用之前对数组进行排序(通过sort方法等)。如果没有对数组进行排序,则结果是不确定的。
如果数组包含多个带有指定值的元素,则无法保证找到的是哪一个

public static void main(String[] args) {
    int[] a = {1,2,323,23,543,12,59};
    Arrays.sort(a); //使用二分法查找,必须先对数组进行排序
    System.out.println("该元素的索引:"+Arrays.binarySearch(a, 12));
}

4、元素填充

public static void main(String[] args) {
    int[] a = {1,2,323,23,543,12,59};
    Arrays.sort(a); //使用二分法查找,必须先对数组进行排序
    Arrays.fill(a, 2, 4, 100); //将2到4索引的元素替换为100
    System.out.println(Arrays.toString(a));
}

5、数组转换为List集合

int[] a = {3,5,1,9,7};
List<int[]> list = Arrays.asList(a);

内存分析

栈 stack:

  1. 每个线程私有,不能实现线程间的共享!
  2. 局部变量放置于栈中。

栈是由系统自动分配,速度快!栈是一个连续的内存空间!

堆 heap:

  1. 放置new出来的对象!
  2. 堆是一个不连续的内存空间,分配灵活,速度慢!

方法区(也是堆):

  1. 被所有线程共享!
  2. 用来存放程序中永远是不变或唯一的内容。(类代码信息、静态变量、字符串常量池

image-20221214005011427

封装继承多态

继承

private 构造器 不行

父类中的属性和方法使用private修饰,在子类中继承后."不可以直接"使用(继承过去了但是无法使用)

static final 修饰的属性和方法也会被继承

super this

父类中的构造器是不能被子类继承的,但是子类的构造器中,会隐式的调用父类中的无参构造器(默认使用super关键字)。

super() 和 this() 都需要是构造函数的第一个语句

super 和 this 不能够同时调用构造方法。(因为this也是在构造方法的第一个语句)

override

static private final方法 不行

  1. static静态方法不能重写

  2. 父类的静态方法不能被子类重写为非静态方法 //编译出错

  3. 父类的非静态方法不能被子类重写为静态方法;//编译出错

  4. 子类可以定义与父类的静态方法同名的静态方法(但是这个不是覆盖)

  5. 私有方法不能被子类重写,子类继承父类后,是不能直接访问父类中的私有方法的,那么就更谈不上重写了。

public class Person{
	private void run(){}
}
//编译通过,但这不是重写,只是俩个类中分别有自己的私有方法
public class Student extends Person{
	private void run(){}
}

重写的语法

  1. 必须存在继承关系。
  2. 方法名和形式参数 必须跟父类是一致的。
  3. 子类的权限修饰符必须要大于或者等于父类的权限修饰符。( private < protected < public,friendly < public )
  4. 子类的返回值类型必须小于或者等于父类的返回值类型。( 子类 < 父类 ) 数据类型没有明确的上下级关系
  5. 方法重写的时候,子类的异常类型小于或者等于父类的异常类型。

多态

能override才能多态

static private protected final 不行

多态存在必须要有“子类重写父类方法”这一条件,那么以下三种类型的方法是没
有办法表现出多态特性的(因为不能被重写):

  1. static方法,因为被static修饰的方法是属于类的,而不是属于实例的
  2. final方法,因为被final修饰的方法无法被子类重写
  3. private方法和protected方法,前者是因为被private修饰的方法对子类不可见,后者是因为尽管被protected修饰的方法可以被子类见到,也可以被子类重写,但是它是无法被外部所引用的,一个不能被外部引用的方法,怎么能谈多态呢

引用类型转换

  • 把子类对象直接赋给父类引用叫upcasting向上转型,向上转型不用强制转型。

  • 把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要强制转型。

  • upcasting 会丢失子类特有的方法,但是子类overriding 父类的方法,子类方法有效

修饰符

static

1、创建和初始化对象的过程

Person s = new Student();

总体原则:先静态后成员 先属性后父类构造器后代码块后构造器

【Student类之前没有进行类加载】

  1. 类加载,同时初始化类中静态的属性
  2. 执行静态代码块
  3. 分配内存空间,同时初始化非静态的属性(赋默认值,0/false/null)
  4. 调用Student的父类构造器
  5. 对Student中的属性进行显示赋值(如果有的话)
  6. 执行匿名代码块
  7. 执行构造器
  8. 返回内存地址

注:子类中非静态属性的显示赋值是在父类构造器执行完之后和子类中的匿名代码块执行之前的时候

public class Person{
    private String name = "zs";
    public Person() {
        System.out.println("Person构造器");  //2
        print();
    }
    public void print(){
        System.out.println("Person print方法: name = "+name);
    }
}


public class Student extends Person{
    private String name = "tom";
    
    {
        System.out.println("Student匿名代码块");	 //3
    }
    static{
        System.out.println("Student静态代码块");  //1
    }
    public Student(){
        System.out.println("Student构造器");	  //4
    }
    public void print(){
        System.out.println("student print方法: name = "+name);
    }
    public static void main(String[] args) {
        new Student();
    }
}

//输出:
Student静态代码块
Person构造器
student print方法: name = null
Student匿名代码块
Student构造器
Student s = new Student();
Student类之前已经进行了类加载
1.分配内存空间,同时初始化非静态的属性(赋默认值,0/false/null)
2.调用Student的父类构造器
3.对Student中的属性进行显示赋值(如果有的话)
4.执行匿名代码块
5.执行构造器
6.返回内存地址

final

1、修饰类

用final修饰的类不能被继承,没有子类

2、修饰方法

用final修饰的方法可以被继承,但是不能被子类的重写

3、修饰变量

用final修饰的变量表示常量,只能被赋一次值.其实使用final修饰的变量也就成了常量了,因为值不会再变了。

abstract

思考1 : 抽象类不能new对象,那么抽象类中有没有构造器?

抽象类是不能被实例化,有构造器,抽象类的目的就是为实现多态中的共同点,抽象类的构造器会在子类实例化时调用,因此它也是用来实现多态中的共同点构造,不建议这样使用!

抽象类中可以没有抽象方法,也可以包含非抽象方法,但有抽象方法的类一定是抽象类。

接口

接口与抽象类的区别

抽象类也是类,除了可以写抽象方法以及不能直接new对象之外,其他的和普通类没有什么不一样的。接口已经另一种类型了,和类是有本质的区别的,所以不能用类的标准去衡量接口。

声明类的关键字是class,声明接口的关键字是interface。
抽象类是用来被继承的,java中的类是单继承。
类A继承了抽象类B,那么类A的对象就属于B类型了,可以使用多态
一个父类的引用,可以指向这个父类的任意子类对象
注:继承的关键字是extends

接口是用来被类实现的,java中的接口可以被多实现。
类A实现接口B、C、D、E…,那么类A的对象就属于B、C、D、E等类型了,可以使用多态
一个接口的引用,可以指向这个接口的任意实现类对象
注:实现的关键字是implements

接口中的方法都是抽象方法

接口中可以不写任何方法,但如果写方法了,该方法必须是抽象方法

接口中的变量都是静态常量(public static final修饰)

接口中可以不写任何属性,但如果写属性了,该属性必须是public static final修饰的静态常量。
注:可以直接使用接口名访问其属性。因为是public static修饰的
注:声明的同时就必须赋值.(因为接口中不能编写静态代码块)

public interface Action{
public static final String NAME = "tom";
//默认就是public static final修饰的
int AGE = 20;
}
main:
System.out.println(Action.NAME);
System.out.println(Action.AGE);

总结

1、Java接口中的成员变量默认都是public,static,final类型的(都可省略),必须被显示初始化,即接
口中的成员变量为常量(大写,单词之间用"_"分隔)
2、Java接口中的方法默认都是public,abstract类型的(都可省略),没有方法体,不能被实例化
3、Java接口中只能包含public,static,final类型的成员变量和public,abstract类型的成员方法
4、接口中没有构造方法,不能被实例化
5、一个接口不能实现(implements)另一个接口,但它可以继承多个其它的接口
6、Java接口必须通过类来实现它的抽象方法
7、当类实现了某个Java接口时,它必须实现接口中的所有抽象方法,否则这个类必须声明为抽象

8、不允许创建接口的实例(实例化),但允许定义接口类型的引用变量,该引用变量引用实现了这
个接口的类的实例
9、 一个类只能继承一个直接的父类,但可以实现多个接口,间接的实现了多继承.

异常

  • 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

异常体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable 作为所有异常的超类。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error异常Exception

image-20221214020214901

image-20221214020320986

异常之间的区别与联系

1、Error

image-20221214020404345

2、Exception

image-20221214020513546

image-20221214020522804

3、检查异常和不受检查异常

image-20221214020541893

image-20221214021048949

集合

image-20221214021852179

重中之重

image-20221214022032347

List

ArrayList

image-20221214022343519 `

1、ArrayList概述

  1. ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。
  2. 该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity【容量】属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。
  3. ArrayList的用法和Vector相类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的

ArrayList和Collection的关系:

image-20221215003558125

2、ArrayList的数据结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IM9AAbKe-1671296934856)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221215004243502.png)]

说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。

3、ArrayList源码分析

1、继承和层次关系

CTRL + H 查看继承关系

image-20221215004415854

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

我们看一下ArrayList的继承结构:
ArrayList extends AbstractList
AbstractList extends AbstractCollection
所有类都继承Object 所以ArrayList的继承结构就是上图这样。

【分析】

  1. 为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
    这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。

  2. ArrayList实现了哪些接口?

List接口:我们会出现这样一个疑问,在查看了ArrayList的父类 AbstractList也实现了List接口,那为什么子类ArrayList还是去实现一遍呢?
这是想不通的地方,所以我就去查资料,有的人说是为了查看代码方便,使观看者一目了然,说法不一,但每一个让我感觉合理的,但是在stackOverFlow中找到了答案,这里其实很有趣。
开发这个collection 的作者Josh说:
这其实是一个mistake[失误],因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。

**RandomAccess接口:**这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如ArrayList。而没有实现该接口的话使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。

Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。

Serializable接口:实现该序列化接口,表明该类可以被序列化.

2、类中的属性
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //版本号
    private static final long serialVersionUID = 8683452581122892189L;
    //Default initial capacity. 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    //Shared empty array instance used for empty instances.
	//空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //缺省空对象数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 元素数组 transient表示不序列化
    transient Object[] elementData;
    // 实际元素大小,默认为0
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
3、构造方法

image-20221215010412996

  1. 无参构造方法

    /*
        Constructs an empty list with an initial capacity of ten.
        这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10.
    */
    //ArrayList中储存数据的其实就是一个数组,这个数组就是elementData.
    public ArrayList() {
        super(); //调用父类中的无参构造方法,父类中的是个空的构造方法
        this.elementData = EMPTY_ELEMENTDATA;
        //EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。
    }
    
  2. 有参构造方法 1

/*
Constructs an empty list with the specified initial capacity.
构造具有指定初始容量的空列表。
@param initialCapacity the initial capacity of the list
初始容量列表的初始容量
@throws IllegalArgumentException if the specified initial capacity isnegative
如果指定的初始容量为负,则为IllegalArgumentException
*/
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
    	//将自定义的容量大小当成初始化 initialCapacity 的大小
    	this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    	this.elementData = EMPTY_ELEMENTDATA; //等同于无参构造方法
    } else {
        //判断如果自定义大小的容量小于0,则报下面这个非法数据异常
        throw new IllegalArgumentException("Illegal Capacity: "+
        initialCapacity);
    }
}
  1. 有参构造方法 2
/*
Constructs a list containing the elements of the specified collection,in the order they are returned by the collection's iterator.
按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表。
@param c the collection whose elements are to be placed into this list
@throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray(); //转换为数组
    //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用ArrayList中的方法去改造一下。
    if ((size = elementData.length) != 0) {
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
    	elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
    	this.elementData = EMPTY_ELEMENTDATA;
    }
}

这个构造方法不常用,举个例子就能明白什么意思
举个例子: Strdent exends Person , ArrayList、 Person这里就是泛型 , 我还有一个Collection,由于这个Student继承了Person,那么根据这个构造方法,我就可以把这个Collection转换为ArrayList, 这就是这个构造方法的作用 。
【总结】ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。

4、核心方法-add
    /**
	Appends the specified element to the end of this list.
	将指定元素追加到此列表的末尾
    Params:
    e – element to be appended to this list
    Returns:
    true (as specified by Collection.add)
    */
        
    public boolean add(E e) {
        //确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;  //在数据中正确的位置上放上元素e,并且size++
        return true;
    }

【分析:ensureCapacityInternal(xxx); 确定内部容量的方法】

 private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 }



private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //判断初始化的elementData是不是空的数组,也就是没有长度
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小。
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用
    return minCapacity;
}




//arrayList核心的方法,能扩展数组大小的真正秘密。
private void grow(int minCapacity) {
    // overflow-conscious code
    //将扩充前的elementData大小给oldCapacity
    int oldCapacity = elementData.length;
    //newCapacity就是1.5倍的oldCapacity
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。
    if (newCapacity - minCapacity < 0)
    	newCapacity = minCapacity;
    //如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    	newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    //新的容量大小已经确定好了,就copy数组,改变容量大小咯。
    elementData = Arrays.copyOf(elementData, newCapacity);
}
总结

正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值容量值。
当我们调用add方法时,实际上的函数调用如下:

image-20221215014054937

4、总结

1)arrayList可以存放null。
2)arrayList本质上就是一个elementData数组。
3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法。
4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而
clear是全是删除集合中的元素。
5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很
多,有移动很多数据才能达到应有的效果
6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环

LinkedList

插入,删除操作频繁时,可使用LinkedList来提高效率。
LinkedList提供对头部和尾部元素进行添加和删除操作的方法!

image-20221215014511839

image-20221215014541077

1、LinkedList概述

image-20221215014806737

LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现
的。
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队
列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。

2、LinkedList的数据结构

image-20221215015004176

如上图所示,LinkedList底层使用的双向链表结构,有一个头结点和一个尾结点,双向链表意味着我们
可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。

3、LinkedList的特性

在我们平常中,我们只知道一些常识性的特点:
1)是通过链表实现的
2)如果在频繁的插入,或者删除数据时,就用linkedList性能会更好。

那我们通过API去查看它的一些特性

1)Doubly-linked list implementation of the `List` and `Deque` interfaces.
Implements all optional list operations, and permits all elements (including
`null`).
这告诉我们,linkedList是一个双向链表,并且实现了List和Deque接口中所有的列表操作,并且能存
储任何元素,包括null,这里我们可以知道linkedList除了可以当链表使用,还可以当作队列使用,并
能进行相应的操作。
2)All of the operations perform as could be expected for a doubly-linked
list. Operations that index into the list will traverse the list from the
beginning or the end, whichever is closer to the specified index.
这个告诉我们,linkedList在执行任何操作的时候,都必须先遍历此列表来靠近通过index查找我们所
需要的的值。通俗点讲,这就告诉了我们这个是顺序存取,每次操作必须先按开始到结束的顺序遍历,随
机存取,就是arrayList,能够通过index。随便访问其中的任意位置的数据,这就是随机列表的意思。

3)api中接下来讲的一大堆,就是说明linkedList是一个非线程安全的(异步),其中在操作Interator时
如果改变列表结构(add delete等),会发生fail-fast。

通过API再次总结一下LinkedList的特性:
1)异步,也就是非线程安全
2)双向链表。由于实现了list和Deque接口,能够当作队列来使用。
链表:查询效率不高,但是插入和删除这种操作性能好。
3)是顺序存取结构(注意和随机存取结构两个概念搞清楚)

4、LinkedList 源码分析

类的属性
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    {
        // 实际元素个数
        transient int size = 0;
        // 头结点
        transient Node<E> first;
        // 尾结点
        transient Node<E> last;
}

LinkedList的属性非常简单,一个头结点、一个尾结点、一个表示链表中实际元素个数的变量。注意,
头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。

构造方法

image-20221215015751743

说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。

内部类(Node)
//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里。
private static class Node<E> {
    E item; // 数据域(当前节点的值)
    Node<E> next; // 后继(指向当前一个节点的后一个节点)
    Node<E> prev; // 前驱(指向当前节点的前一个节点)
    // 构造函数,赋值前驱后继
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

说明:内部类Node就是实际的结点,用于存放实际元素的地方。

4、总结
  1. linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
  2. 能存储null值
  3. 跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查
    询的性能上好
  4. 从源码中看,它不存在容量不足的情况
  5. linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能
    移除值。
  6. linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。

Vector和Stack

前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的。这
一篇讲的可能大家在开发中很少去用到。但是有的时候也可能是会用到的!
注意在学习这一篇之前,需要有多线程的知识:

  1. 锁机制:对象锁、方法锁、类锁
    对象锁就是方法锁:就是在一个类中的方法上加上synchronized关键字,这就是给这个方法加锁了。
    类锁:锁的是整个类,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象
    被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。
    其他线程继续被阻塞住。例如:在类A上有关键字synchronized,那么就是给类A加了类锁,线程1第一
    个声明此类的实例,则线程1拿到了该类锁,线程2在想声明类A的对象,就会被阻塞。
  2. 在本文中,使用的是方法锁。
  3. 每个对象只有一把锁,有线程A,线程B,还有一个集合C类,线程A操作C拿到了集合中的锁(在
    集合C中有用synchronized关键字修饰的),并且还没有执行完,那么线程A就不会释放锁,当轮到线程B
    去操作集合C中的方法时 ,发现锁被人拿走了,所以线程B只能等待那个拿到锁的线程使用完,然后才能
    拿到锁进行相应的操作。

Vector

1、Vector概述

image-20221215174014220

通过API中可以知道:

  1. Vector是一个可变化长度的数组
  2. Vector增加长度通过的是capacity和capacityIncrement这两个变量,目前还不知道如何实现自动
    扩增的,等会源码分析
  3. Vector也可以获得iterator和listIterator这两个迭代器,并且他们发生的是fail-fast,而不是failsafe,
    注意这里,不要觉得这个vector是线程安全就搞错了,具体分析在下面会说
  4. Vector是一个线程安全的类,如果使用需要线程安全就使用Vector,如果不需要,就使用arrayList
  5. Vector和ArrayList很类似,就少许的不一样,从它继承的类和实现的接口来看,跟arrayList一模一
    样。
    注意:现在的版本已经是jdk1.7,还有更高的jdk1.8了,在开发中,建议不用vector,原因在文章的
    结束会有解释,如果需要线程安全的集合类直接用java.util.concurrent包下的类。

Stack

现在来看看Vector的子类Stack,学过数据结构都知道,这个就是栈的意思。那么该类就是跟栈的用法一
样了

class Stack<E> extends Vector<E> {}

通过查看他的方法,和查看api文档,很容易就能知道他的特性。就几个操作,出栈,入栈等,构造方法
也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也
线程安全

总结

【Vector总结(通过源码分析)】

  1. Vector线程安全是因为它的方法都加了synchronized关键字
  2. Vector的本质是一个数组,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关
  3. 它也会fail-fast,还有一个fail-safe两个的区别在下面的list总结中会讲到。

【Stack的总结】

  1. 对栈的一些操作,先进后出
  2. 底层也是用数组实现的,因为继承了Vector
  3. 也是线程安全的

List总结

【arrayList和LinkedList区别】

arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长
度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性
能更好

LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。
在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性
能更好。
两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效

【arrayList和Vector的区别】

arrayList线程不安全,在用iterator,会发生fail-fast
Vector线程安全,因为在方法前加了Synchronized关键字。也会发生fail-fast

【fail-fast和fail-safe区别和什么情况下会发生】

简单的来说:在java.util下的集合都是发生fail-fast,而在java.util.concurrent下的发生的都是failsafe

1)fail-fast
快速失败,例如在arrayList中使用迭代器遍历时,有另外的线程对arrayList的存储数组进行了改变,比
如add、delete、等使之发生了结构上的改变,所以Iterator就会快速报一个
java.util.ConcurrentModificationException 异常(并发修改异常),这就是快速失败。

2)fail-safe

安全失败,在java.util.concurrent下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行
结构的改变,不会报异常,而是正常遍历,这就是安全失败。

3)为什么在java.util.concurrent包下对集合有结构的改变,却不会报异常?
在concurrent下的集合类增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素,如果有
其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍
历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出
现一个副本。所以能防止fail-fast,这种机制并不会出错,所以我们叫这种现象为fail-safe。

4)vector也是线程安全的,为什么是fail-fast呢?
这里搞清楚一个问题,并不是说线程安全的集合就不会报fail-fast,而是报fail-safe,你得搞清楚前面所
说答案的原理,出现fail-safe是因为他们在实现增删的底层机制不一样,就像上面说的,会有一个副
本,而像arrayList、linekdList、verctor等,他们底层就是对着真正的引用进行操作,所以才会发生异
常。

5)既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢(也就是对其
删除和增加等操作)?
首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿vector来说,我们都没
有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办
法,因为那把锁就放在那边。

【举例说明fail-fast和fail-safe的区别】

  1. fail-fast

image-20221215193647866

  1. fail-safe

通过CopyOnWriteArrayList这个类来做实验,不用管这个类的作用,但是他确实没有报异常,
并且还通过第二次打印,来验证了上面我们说创建了副本的事情。
原理是在添加操作时会创建副本,在副本上进行添加操作,等迭代器遍历结束后,会将原引用
改为副本引用,所以我们在创建了一个list的迭代器,结果打印的就是123444了,
证明了确实改变成为了副本引用,后面为什么是三个4,原因是我们循环了3次,不久添加了3
个4吗。如果还感觉不爽的话,看下add的源码。

image-20221215193915783

CopyOnWriteArrayList list = new CopyOnWriteArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

Iterator iterator = list.iterator();
while (iterator.hasNext()){
    list.add(5);
    System.out.print(iterator.next() + " ");
}

System.out.println();
Iterator iterator1 = list.iterator();
while (iterator1.hasNext()){
    System.out.print(iterator1.next() + " ");
}
//1 2 3 4 
//1 2 3 4 5 5 5 5 

【为什么现在都不提倡使用vector了】

1)vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中,
一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安
全。
2)如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,vector
本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销。
3)就如上面第三个问题所说的,vector还有fail-fast的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用arrayList,然后再加锁呢。
总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在
jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的
类。

Map

HashMap

1. HashMap数据结构

1.1 HashMap概述

HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。
在API中给出了相应的定义:

//1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value,可以为null,(HashMap和HashTable大致上是一样的,除了hashmap是异步的,和允许key和value为null),
//这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
Hash table based implementation of the Map interface. This implementation
provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
    
//假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)成正比,因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】
This implementation provides constant-time performance for the basic
operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the
"capacity" of the HashMap instance (the number of buckets) plus its size(the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (orthe load factor too low) if iteration performance is important.
    
//HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量,
在容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量.
An instance of HashMap has two parameters that affect its performance:
initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity,the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
    
//通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff
between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum
number of entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的
容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table
1.2 HashMap在JDK1.8以前数据结构和存储原理

【链表散列】

首先我们要知道什么是链表散列?通过数组和链表结合在一起使用,就叫做链表散列。这其实就是
hashmap存储的原理图。

image-20221215223836959

【HashMap的数据结构和存储原理】

HashMap的数据结构就是用的链表散列。那HashMap底层是怎么样使用这个数据结构进行数据存取的呢?分成两个部分:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key
和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key; //就是我们说的map的key
    V value; //value值,这两个都不陌生
    Entry<K,V> next;//指向下一个entry对象
    int hash;//通过key算过来的你hashcode值。
}

image-20221215224123652

第二步:构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。
大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。

image-20221215224224362

【Hash存放元素的过程】

通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash值数组的长度length来计算出entry放在数组中的哪个位置上面,每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash值来确定将该对象存放在数组中的哪个位置上。

1.3 JDK1.8后HashMap的数据结构
image-20221215224655951

上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。

1.4 HashMap的属性

HashMap的实例有两个参数影响其性能。
初始容量:哈希表中桶的数量(桶就是数组的容量)
加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

【loadFactor加载因子】

定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为
0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。

【桶】

根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里
就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。

【capacity】
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是
16。

一般第一次扩容时会扩容到64之后好像是2倍。总之,容量都是2的幂。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

【size的含义】
size就是在该HashMap的实例中实际存储的元素的个数

【threshold的作用】

int threshold;

threshold = capacity * loadFactor,当Size>=threshold ==(元素个数 > 数组容量 * 0.75)==的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条
件。

什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,
这才能扩增数组。

我们通过一张HashMap的数据结构图来分析:

image-20221215230816272

2. HashMap的源码分析

2.1 HashMap的层次关系与继承结构

【HashMap继承结构】

image-20221215232006833

上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。

【实现接口】

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}

Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合
都有这样的错误,也没过大影响
Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响
被拷贝的对象。
Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。

2.2HashMap类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 临界值 当实际大小(size*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 填充因子
    final float loadFactor;
}

2.3 HashMap的构造方法

有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数
组的长度。)和初始化加载因子。注意,hashMap中table数组一开始就已经是个没有长度的数组了。

构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个
是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。

【HashMap()】

//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75
//初始化容量:也就是初始化数组的大小
//加载因子:数组上的存放数据疏密程度。
public HashMap() {
	this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

【HashMap(int)】

public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

【HashMap(int,float)】

public HashMap(int initialCapacity, float loadFactor) {
    // 初始容量不能小于0,否则报错
    if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
    initialCapacity);
    // 初始容量不能大于最大值,否则为最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
    // 填充因子不能小于或等于0,不能为非数字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
    loadFactor);
    // 初始化填充因子
    this.loadFactor = loadFactor;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);
}

【HashMap(Map<? extends K, ? extends V> m)】

public HashMap(Map<? extends K, ? extends V> m) {
	// 初始化填充因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 将m中的所有元素添加至HashMap中
    putMapEntries(m, false);
}

3. 常用方法

【put(K key,V value)】

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

【putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)】

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 将第一个元素赋值给e,用e来记录
            e = p;
        // hash值不相等,即key不相等;为红黑树结点
        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);
                    // 结点数量达到阈值,转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插
入元素的。

4. 总结

【关于数组扩容】
从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就
会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,
此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还
有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处
理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍
历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各
个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于
元素在桶中均匀分布所带来的好处。

【总结】

  1. 要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加
    链表加红黑树的数据结构。
  2. 通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的
    是hash查找值得方法。

迭代器

所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象

Iterator对象称作为迭代器,用以方便的对容器内元素的遍历操作,Iterator接口定义了如下方法:

  • boolean hashNext();//判断是否有元素没有被遍历
  • Object next();//返回游标当前位置的元素并将游标移动到下一个位置
  • void remove();//删除游标左边的元素,在执行完next之后该操作只能执行一次

执行next后才能进行remove,不然会异常fail-fast

问题:何遍历Map集合呢?

方法1:通过迭代器Iterator实现遍历 KeySet
获取Iterator :Collection 接口的iterator()方法
Iterator的方法:

  • boolean hasNext(): 判断是否存在另一个可访问的元素
  • Object next(): 返回要访问的下一个元素
HashMap dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");

Set keys=dogMap.keySet(); //取出所有key的集合
Iterator it=keys.iterator(); //获取Iterator对象
while(it.hasNext()){
    String key=(String)it.next(); //取出key
    System.out.println(dogMap.get(key)); //根据key取出对应的值
}

方法2:通过迭代器Iterator实现遍历EntrySet

HashMap<String,String> dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");


//HashMap定义的时候要加泛型这边才能自动生成
Iterator<Map.Entry<String, String>> iterator = dogMap.entrySet().iterator();
while (iterator.hasNext()){
    Map.Entry<String, String> next = iterator.next();
    System.out.println(next.getValue());
}

方法3:foreach

HashMap<String,String> dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");

for (Map.Entry<String, String> stringStringEntry : dogMap.entrySet()) {
    System.out.println(stringStringEntry.getValue());
}

泛型

E:Element,在集合中使用,因为集合中存放的是元素。

T:Type,Java类。

K:Key,键。

V:Value,值。

N:Number,数值类型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许
程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

如何解决以下强制类型转换时容易出现的异常问题?
List的get(int index)方法获取元素
Map的get(Object key)方法获取元素
Iterator的next()方法获取元素
分析:通过泛型 , JDK1.5使用泛型改写了集合框架中的所有接口和类

image-20221216002433870

image-20221216002520933

? 通配符: < ? >

Collections工具类

【前言】
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进
行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
这个类不需要创建对象,内部提供的都是静态方法。

2、排序操作

1static void reverse(List<?> list):
反转列表中元素的顺序。
2static void shuffle(List<?> list) :List集合元素进行随机排序。
3static void sort(List<T> list)
根据元素的自然顺序 对指定列表按升序进行排序
4static <T> void sort(List<T> list, Comparator<? super T> c) :
根据指定比较器产生的顺序对指定列表进行排序。
5static void swap(List<?> list, int i, int j)
在指定List的指定位置i,j处交换元素。
6static void rotate(List<?> list, int distance)
当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为
负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。

3、查找、替换操作

1static <T> int binarySearch(List<? extends Comparable<? super T>>
list, T key)
使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。
注意:此前必须保证List集合中的元素已经处于有序状态。
2static Object max(Collection coll)
根据元素的自然顺序,返回给定collection 的最大元素。
3static Object max(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最大元素。
4static Object min(Collection coll):
根据元素的自然顺序,返回给定collection 的最小元素。
5static Object min(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最小元素。
6static <T> void fill(List<? super T> list, T obj) :
使用指定元素替换指定列表中的所有元素。
7static int frequency(Collection<?> c, Object o)
返回指定 collection 中等于指定对象的出现次数。
8static int indexOfSubList(List<?> source, List<?> target) :
返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回
-19static int lastIndexOfSubList(List<?> source, List<?> target)
返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回
-110static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
使用一个新值替换List对象的所有旧值oldVal

4、同步控制

Collectons提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从
而解决多线程并发访问集合时的线程安全问题。
正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。
Collections提供了多个静态方法可以把他们包装成线程同步的集合。

1static <T> Collection<T> synchronizedCollection(Collection<T> c)
返回指定 collection 支持的同步(线程安全的)collection。
2static <T> List<T> synchronizedList(List<T> list)
返回指定列表支持的同步(线程安全的)列表。
3static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定映射支持的同步(线程安全的)映射。
4static <T> Set<T> synchronizedSet(Set<T> s)
返回指定 set 支持的同步(线程安全的)set。
import java.util.*;
public class TestSynchronized
{
    public static void main(String[] args)
        {
        //下面程序创建了四个同步的集合对象
        Collection c = Collections.synchronizedCollection(new ArrayList());
        List list = Collections.synchronizedList(new ArrayList());
        Set s = Collections.synchronizedSet(new HashSet());
        Map m = Collections.synchronizedMap(new HashMap());
        }
}

IO

1. 输入输出流分类

Java.io 包中定义了多个流类型(类或抽象类)来实现输入/输出功能;可以从不同的角度对其进行分类:

  • 按数据流的方向不同可以分为输入流和输出流
  • 按照处理数据单位不同可以分为字节流和字符流
  • 按照功能不同可以分为节点流和处理流

我们来理解两个概念:
字节流:最原始的一个流,读出来的数据就是010101这种最底层的数据表示形式,只不过它是按照字节来读的,一个字节(Byte)是8位(bit)读的时候不是一个位一个位的来读,而是一个字节一个字节来读。
字符流:字符流是一个字符一个字符地往外读取数据。一个字符是2个字节
J2SDK所提供的所有流类型位于包 Java.io内,都分别继承自以下四种抽象流类型
输入流:InputStream(字节流),Reader(字符流)
输出流:OutPutStream(字节流),Writer(字符流)
这四个类都是抽象类,可以把这四个类想象成四根不同的管道。一端接着你的程序,另一端接着数据源,你可以通过输出管道从数据源里面往外读数据,也可以通过输入管道往数据源里面输入数据,总之,通过这四根管道可以让数据流进来和流出去。
io包里面定义了所有的流,所以一说流指的就是io包里面的

什么叫输入流?什么叫输出流?
用一根管道一端插进文件里,一端插进程序里面,然后开始读数据,那么这是输入还是输出呢?
如果站在文件的角度上,这叫输出。
如果站在程序的角度上,这叫输入。
记住,以后说输入流和输出流都是站在程序的角度上来说。

2. 节点流和处理流

image-20221216172942624

你要是对原始的流不满意,你可以在这根管道外面再套其它的管道,套在其它管道之上的流叫处理流。
为什么需要处理流呢?这就跟水流里面有杂质,你要过滤它,你可以再套一层管道过滤这些杂质一样。

2.1.节点流类型

image-20221216173043730

节点流就是一根管道直接插到数据源上面,直接读数据源里面的数据,或者是直接往数据源里面写入数据。典型的节点流是文件流:文件的字节输入流(FileInputStream),文件的字节输出流(FileOutputStream),文件的字符输入流(FileReader),文件的字符输出流(FileWriter)。

2.2.处理流类型

image-20221216173223455

处理流是包在别的流上面的流,相当于是包到别的管道上面的管道。

3. InputStream(输入流)

我们看到的具体的某一些管道,凡是以InputStream结尾的管道,都是以字节的形式向我们的程序输入数据。
继承自InputStream的流都是用于向程序中输入数据,且数据的单位为字节(8bit);下图中深色为节点流,浅色为处理流。

image-20221216173409018

3.1.InputStream的基本方法

//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException

read()方法是一个字节一个字节地往外读,每读取一个字节,就处理一个字节。read(byte[] buffer)方法读取数据时,先把读取到的数据填满这个byte[]类型的数组buffer(buffer是内存里面的一块缓冲区),然后再处理数组里面的数据。这就跟我们取水一样,先用一个桶去接,等桶接满水后再处理桶里面的水。
如果是每读取一个字节就处理一个字节,这样子读取也太累了。

3.2 案例

public class TestFileInputStream {
    public static void main(String args[]) {
        int b = 0;// 使用变量b来装调用read()方法时返回的整数
        FileInputStream in = null;
		// 使用FileInputStream流来读取有中文的内容时,读出来的是乱码,因为使用InputStream流里面的read()方法读取内容时是一个字节一个字节地读取的,而一个汉字是占用两个字节的,所以读取出来的汉字无法正确显示。
		// FileReader in = null;
		// 使用FileReader流来读取内容时,中英文都可以正确显示,因为Reader流里面的read()方法是一个字符一个字符地读取的,这样每次读取出来的都是一个完整的汉字,这样就可以正确显示了。
        try {
            in = new FileInputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
            // in = new FileReader("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
        } catch (FileNotFoundException e) {
            System.out.println("系统找不到指定文件!");
            System.exit(-1);// 系统非正常退出
        }
        long num = 0;// 使用变量num来记录读取到的字符数
		// 调用read()方法时会抛异常,所以需要捕获异常
        try {
            while ((b = in.read()) != -1) {
            // 调用int read() throws Exception方法时,返回的是一个int类型的整数
            // 循环结束的条件就是返回一个值-1,表示此时已经读取到文件的末尾了。
            // System.out.print(b+"\t");//如果没有使用“(char)b”进行转换,那么直接打印出来的b就是数字,而不是英文和中文了
                System.out.print((char) b);
				// “char(b)”把使用数字表示的汉字和英文字母转换成字符输入
                num++;
            }
            in.close();// 关闭输入流
            System.out.println();
            System.out.println("总共读取了" + num + "个字节的文件");
        } catch (IOException e1) {
            System.out.println("文件读取错误!");
        }
    }
}

4. OutputStream(输出流)

继承自OutputStream的流是用于程序中输出数据,且数据的单位为字节(8bit):下图中深色的为节点流,浅色为处理流。

image-20221216174735192

4.1.OutputStream的基本方法

//向输出流中写入一个字节数据,该字节数据为参数b的低8位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException

4.2 案例

public class TestFileOutputStream {
    public static void main(String args[]) {
        int b = 0;
        FileInputStream in = null;
        FileOutputStream out = null;
        try {
            in = new FileInputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
            out = new FileOutputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
                    // 指明要写入数据的文件,如果指定的路径中不存在StudentNew.java这样的文件,则系统会自动创建一个
            while ((b = in.read()) != -1) {
                out.write(b);
                // 调用write(int c)方法把读取到的字符全部写入到指定文件中去
            }
            in.close();
            out.close();
        } catch (FileNotFoundException e) {
            System.out.println("文件读取失败");
            System.exit(-1);// 非正常退出
        } catch (IOException e1) {
            System.out.println("文件复制失败!");
            System.exit(-1);
        }
        System.out
                .println("Student.StudentNew.java里面");
    }
}

5. Reader流

Reader : 和InputStream一模一样,唯一的区别就在于读的数据单位不同

继承自Reader的流都是用于向程序中输入数据,且数据的单位为字符(16bit)

16位:一个字符也就是两个字节,使用Reader流读取数据时都是两个字节两个字节往外读的,为什么还要有这两种两个字节的读取方式呢? 因为有些字符是占2个字节的,如我们的中文字符在Java里面就是占两个字节的。如果采用一个字节一个字节往外读的方式,那么读出来的就是半个汉字,这样子Java就没有办法正确的显示中文字符的,所以有必要存在这种流,一个字符一个字符地往外读。

5.1.Reader的基本方法

//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException

6. Writer流

继承自Writer的流都是用于程序中输出数据,且数据的单位为字符(16bit);

6.1.Writer的基本方法

//向输出流中写入一个字节数据,该字节数据为参数b的低16位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException

6.2 演示

/*使用FileWriter(字符流)向指定文件中写入数据写入数据时以1个字符为单位进行写入*/
import java.io.*;
public class TestFileWriter{
    public static void main(String args[]){
        /*使用FileWriter输出流从程序把数据写入到Uicode.dat文件中
        使用FileWriter流向文件写入数据时是一个字符一个字符写入的*/
        FileWriter fw = null;
        try{
            fw = new FileWriter("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
            //字符的本质是一个无符号的16位整数
            //字符在计算机内部占用2个字节
            //这里使用for循环把0~60000里面的所有整数都输出
            //这里相当于是把全世界各个国家的文字都0~60000内的整数的形式来表示
            for(int c=0;c<=60000;c++){
                fw.write(c);
                //使用write(int c)把0~60000内的整数写入到指定文件内
                //调用write()方法时,我认为在执行的过程中应该使用了“(char)c”进行强制转换,即把整数转换成字符来显示
                //因为打开写入数据的文件可以看到,里面显示的数据并不是0~60000内的整数,而是不同国家的文字的表示方式
            }
            /*使用FileReader(字符流)读取指定文件里面的内容
            读取内容时是以一个字符为单位进行读取的*/
            int b = 0;
            long num = 0;
            FileReader fr = null;
            fr = new FileReader("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
            while((b = fr.read())!= -1){
                System.out.print((char)b + "\t");
                num++;
            }
            System.out.println();
            System.out.println("总共读取了"+num+"个字符");
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
}

FileReader和FileWriter这两个流都是字符流,都是以一个字符为单位进行输入和输出的。所以读取和写入占用2个字节的字符时都可以正常地显示出来,以上是以File(文件)这个类型为例对节点流进行了讲解,所谓的节点流指定就是直接把输入流或输出插入到数据源上,直接往数据源里面写入数据或读取数据。

7. 处理流讲解

7.1.第一种处理流——缓冲流(Buffering)

缓冲流要”套接“在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加了一些新的方法。J2SDK提供了四种缓冲流,常用构造方法如下:

BufferedReader(Reader in)
BufferedReader(Reader in,int sz) //sz 为自定义缓冲区的大小
BufferedWriter(Writer out)
BufferedWriter(Writer out,int sz)
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in,int size)
BufferedOutputStream(InputStream in)
BufferedOutputStream(InputStream in,int size)
  • 缓冲输入流支持其父类的mark和reset方法。
  • BufferedReader提供了readLine方法用于读取一行字符串
  • BufferedWriter提供了newLine用于写入一个行分隔符
  • 对于输出的缓冲流,写出的数据会现在内存中缓存,使用flush方法将会使内存中的数据立刻写出

带有缓冲区的,缓冲区(Buffer)就是内存里面的一小块区域,读写数据时都是先把数据放到这块缓冲区
域里面,减少io对硬盘的访问次数,保护我们的硬盘。可以把缓冲区想象成一个小桶,把要读写的数据想象成水,每次读取数据或者是写入数据之前,都是先把数据装到这个桶里面,装满了以后再做处理。
这就是所谓的缓冲。先把数据放置到缓冲区上,等到缓冲区满了以后,再一次把缓冲区里面的数据写入到硬盘上或者读取出来,这样可以有效地减少对硬盘的访问次数,有利于保护我们的硬盘。

【缓冲流测试代码:BufferedInputStream】

public class Java01_Variable {
    public static void main(String[] args)  {
        try {
            FileInputStream in = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
            FileInputStream out = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt.copy");

            BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
            System.out.println((char)bufferedInputStream.read());

            int c = 0;
            bufferedInputStream.mark(10);// 在第10个字符处做一个标记
            for (int i = 0; i < 10 && (c = bufferedInputStream.read()) != -1; i++) {
                System.out.print((char) c);
            }
            bufferedInputStream.reset();// 重新回到原来标记的地方
            System.out.println();
            for (int i = 0; i < 10 && (c = bufferedInputStream.read()) != -1; i++) {
                System.out.print((char) c);
            }

            bufferedInputStream.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }



    }
}

【演示:BufferedReader BufferedWriter】

public class Java01_Variable {
    public static void main(String[] args)  {
        try {
            FileWriter out = new FileWriter("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
            //在节点流FileWriter的外面再套一层处理流BufferedWriter
            BufferedWriter bw = new BufferedWriter(out);

            String s = null;
            for(int i=0;i<100;i++){
                //“Math.random()”将会生成一系列介于0~1之间的随机数。
                // static String valueOf(double d)这个valueOf()方法的作用就是把一个double类型的数转换成字符串
                //valueOf()是一个静态方法,所以可以使用“类型.静态方法名”的形式来调用
                s = String.valueOf(Math.random());
                bw.write(s);//把随机数字符串写入到指定文件中
                bw.newLine();//调用newLine()方法使得每写入一个随机数就换行显示
            }
            bw.flush();//调用flush()方法清空缓冲区

            //在节点流FileReader的外面再套一层处理流BufferedReader
            BufferedReader br = new BufferedReader(new FileReader("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt"));
            while((s = br.readLine())!=null){
            //使用BufferedReader处理流里面提供String readLine()方法读取文件中的数据时是一行一行读取的
            //循环结束的条件就是使用readLine()方法读取数据返回的字符串为空值后则表示已经读取到文件的末尾了。
                System.out.println(s);
            }
            bw.close();
            br.close();

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }



    }
}

image-20221216194257740

程序的输入指的是把从文件读取到的内容存储到为程序分配的内存区域里面去。流,什么是流,流无非就是两根管道,一根向里,一根向外,向里向外都是对于我们自己写的程序来说,流分为各种各样的类型,不同的分类方式又可以分为不同的类型,根据方向来分,分为输入流和输出流,根据读取数据的单位的不同,又可以分为字符流和字节流,除此之外,还可以分为节点流和处理流,节点流就是直接和数据源连接的流,处理流就是包在其它流上面的流,处理流不是直接和数据源连接,而是从数据源读取到数据以后再通过处理流处理一遍。缓冲流也包含了四个类:BufferedInputStream、BufferedOutputStream、BufferedReader和BufferedWriter。流都是成对的,没有流是是不成对的,肯定是一个in,一个out。

7.2.第二种处理流——转换流

InputStreamReader 和 OutputStreamWriter 用于字节数据到字符数据之间的转换
InputStreamReader 需要和 InputStream “套接” 。
OutputStreamWriter 需要和 OutputStream “套接” 。
转换流在构造时可以指定其编码集合

//把字节流转为字符流
InputStream isr = new InputStreamReaderSystem.in,"ISO8859-1"

转换流非常的有用,它可以把一个字节流转换成一个字符流,转换流有两种,一种叫InputStreamReader,另一种叫OutputStreamWriter。InputStream是字节流,Reader是字符流,InputStreamReader就是把InputStream转换成Reader。OutputStream是字节流,Writer是字符流,OutputStreamWriter就是把OutputStream转换成Writer。把OutputStream转换成Writer之后就可以一个字符一个字符地通过管道写入数据了,而且还可以写入字符串。我们如果用一个FileOutputStream流往文件里面写东西,得要一个字节一个字节地写进去,但是如果我们在FileOutputStream流上面套上一个字符转换流,那我们就可以一个字符串一个字符串地写进去。

【转换流测试代码】

public class Java01_Variable {
    public static void main(String[] args)  {
        try {
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt"));
            outputStreamWriter.write("QIUYUSY");
            // 使用getEncoding()方法取得当前系统的默认字符编码
            outputStreamWriter.getEncoding();

            outputStreamWriter.close();

            //如果在调用FileOutputStream的构造方法时没有加入true,那么新加入的字符
            //串就会替换掉原来写入的字符串,在调用构造方法时指定了字符的编码
            outputStreamWriter = new OutputStreamWriter(
                    new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt",true),"ISO8859_1");

            outputStreamWriter.write("MircosoftsunIBMOracleApplet");// 再次向指定的文件写入字符串,新写入的字符串加入到原来字符串的后面
            System.out.println(outputStreamWriter.getEncoding());
            outputStreamWriter.close();

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }



    }
}

image-20221216195356110

public static void main(String[] args) {
    try {
        //System.in这里的in是一个标准的输入流,用来接收从键盘输入的数据
        InputStreamReader inputStreamReader = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(inputStreamReader);
        String s = null;
        s = br.readLine();//使用readLine()方法把读取到的一行字符串保存到字符串变量s中去
        while (s != null) {
            System.out.println(s.toUpperCase());//把保存在内存s中的字符串打印出来
            s = br.readLine();//在循环体内继续接收从键盘的输入
            if (s.equalsIgnoreCase("exit")) {
                //只要输入exit循环就结束,就会退出
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

image-20221216195927242

7.3 第三种处理流——数据流

DataInputStream 和 DataOutputStream 分别继承自InputStream 和 OutputStream , 它属于处
理流,需要分别“套接”在InputStream 和 OutputStream类型的节点流上。
DataInputStream 和 DataOutputStream 提供了可以存取与机器无关的Java原始类型数据(int,
double等)的方法。
DataInputStream 和 DataOutputStream 的构造方法

DataInputStreamInputStream in)
DataOutputStreamOutputStream out)

【数据流测试代码】

public static void main(String[] args) {
    //在调用构造方法时,首先会在内存里面创建一个ByteArray字节数组
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    //在输出流的外面套上一层数据流,用来处理int,double类型的数
    DataOutputStream dos = new DataOutputStream(baos);

    try{
        dos.writeDouble(Math.random());//把产生的随机数直接写入到字节数组ByteArray中
        dos.writeBoolean(true);//布尔类型的数据在内存中就只占一个字节
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        System.out.println(bais.available());
        DataInputStream dis = new DataInputStream(bais);
        System.out.println(dis.readDouble());//先写进去的就先读出来,调用readDouble()方法读取出写入的随机数
        System.out.println(dis.readBoolean());//后写进去的就后读出来,这里面的读取顺序不能更改位置,否则会打印出不正确的结果
        dos.close();
        bais.close();
    }catch(Exception e){
        e.printStackTrace();
    }

}

7.4.打印流——Print

PrintWriter 和 PrintStream 都属于输出流,分别针对与字符和字节
PrintWriter 和 PrintStream 提供了重载的print
Println方法用于多种数据类型的输出
PrintWriter和PrintStream的输出操作不会抛出异常,用户通过检测错误状态获取错误信息
PrintWriter 和 PrintStream有自动flush功能

PrintWriterWriter out)
PrintWriterWriter out,boolean autoFlush)
PrintWriterOutputStream out)
PrintWriterOutputStream out,boolean autoFlush)
PrintStreamOutputStream out)
PrintStreamOutputStream out,boolean autoFlush)
/*这个小程序是重新设置打印输出的窗口,
* 把默认在命令行窗口输出打印内容设置成其他指定的打印显示窗口
*/
public static void main(String[] args) {
    PrintStream printStream = null;
    try {
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
        //在输出流的外面套接一层打印流,用来控制打印输出
        printStream = new PrintStream(fileOutputStream);
        if(printStream != null){
            //这里调用setOut()方法改变了输出窗口,以前写System.out.print()默认的输出窗口就是命令行窗口.
            //但现在使用System.setOut(ps)将打印输出窗口改成了由ps指定的文件里面,通过这样设置以后,打印输出时都会在指定的文件内打印输出
            //在这里将打印输出窗口设置到了log.txt这个文件里面,所以打印出来的内容会在log.txt这个文件里面看到
            System.setOut(printStream);
        }

        for (char c = 0; c < 1000; c++) {
            System.out.print(c+"\t");
        }
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }
}

image-20221216201142653

7.5. 对象流——Object

直接将Object 写入或读出

  • transient关键字
    • transient:透明的,用它来修饰的成员变量在序列化的时候不予考虑,也就是当成不存在。
  • serializable接口
  • externaliazble接口
public class Java01_Variable {
    public static void main(String[] args) {
        Student student = new Student();
        student.k = 8;// 把k的值修改为8

        try {
            FileOutputStream fileOutputStream = new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
            // ObjectOutputStream流专门用来处理Object的
            // 在fos流的外面套接ObjectOutputStream流就可以直接把一个Object写进去
            ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
            oos.writeObject(student);// 直接把一个t对象写入到指定的文件里面
            oos.flush();
            oos.close();

            FileInputStream fileInputStream = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
            // ObjectInputStream专门用来读一个Object的
            ObjectInputStream ois = new ObjectInputStream(fileInputStream);
            // 直接把文件里面的内容全部读取出来然后分解成一个Object对象,并使用强制转换成指定类型T
            Student sRead = (Student)ois.readObject();
            System.out.println(sRead);
            //Student{i=10, j=9, d=2.3, k=0} 可以看到k没有被序列化

            ois.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }
}

/*
* 凡是要将一个类的对象序列化成一个字节流就必须实现Serializable接口
* Serializable接口中没有定义方法,Serializable接口是一个标记性接口,用来给类作标记,只是起到一个标记作用。
* 这个标记是给编译器看的,编译器看到这个标记之后就可以知道这个类可以被序列化 如果想把某个类的对象序列化,就必须得实现Serializable接口
*/
class Student implements Serializable {
    // Serializable的意思是可以被序列化的
    int i = 10;
    int j = 9;
    double d = 2.3;
    transient int k = 15;
// 在声明变量时如果加上transient关键字,那么这个变量就会被当作是透明的,即不存在。


    @Override
    public String toString() {
        return "Student{" +
                "i=" + i +
                ", j=" + j +
                ", d=" + d +
                ", k=" + k +
                '}';
    }
}

直接实现Serializable接口的类是JDK自动把这个类的对象序列化,而如果实现public interface
Externalizable extends Serializable的类则可以自己控制对象的序列化,建议能让JDK自己控制序列化
的就不要让自己去控制

8.IO流总结

image-20221216202634643

多线程

线程简介

image-20221216223340104

本章核心概念

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  • main() 称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建(重点)

1. 继承Thread类(重点)

image-20221216224257917

例子:多线程图片下载器

下载图片需要先导入

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * @author QiuYuSY
 */

//练习Thread,实现多线程同步下载图片
public class TestThread extends Thread {
    private String url;
    private String name;

    public TestThread(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("文件名为"+name+"的文件下载完毕!");
    }

    public static void main(String[] args) {
        TestThread t1 = new TestThread("https://www.kuangstudy.com/assert/course/c1/14.jpg","test1.jpg");
        TestThread t2 = new TestThread("https://www.kuangstudy.com/assert/course/c1/13.jpg","test2.jpg");
        TestThread t3 = new TestThread("https://www.kuangstudy.com/assert/course/c1/11.jpg","test3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}

//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url, String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));

        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("io异常,downloader出现问题");
        }
    }
}

2. 实现Runnable接口(重点)

image-20221216231448242

public class TestThread implements Runnable {
    private String url;
    private String name;

    public TestThread(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("文件名为"+name+"的文件下载完毕!");
    }

    public static void main(String[] args) {
        //需要在Thread中new出继承Runable的对象
        Thread t1 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/14.jpg", "test1.jpg"));
        Thread t2 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/13.jpg", "test2.jpg"));
        Thread t3 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/11.jpg", "test3.jpg"));

        t1.start();
        t2.start();
        t3.start();
    }
}

image-20221216232049285

多线程抢票问题

package org.example;

/**
 * @author QiuYuSY
 * @create 2022-12-16 23:21
 */

//多线程操作一个对象
//以买火车票为例子
public class TestThread4 implements Runnable {
    private int ticketNums = 10;


    @Override
    public void run() {
        while (true){
            if(ticketNums <= 0){
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第"+ticketNums--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket= new TestThread4();

        new Thread(ticket,"A").start();
        new Thread(ticket,"B").start();
        new Thread(ticket,"C").start();
    }
}

//结果如下,这种时候就发生资源抢占的问题
B拿到了第8张票
C拿到了第10张票
A拿到了第9张票
B拿到了第7张票
C拿到了第7张票
A拿到了第6张票
C拿到了第5张票
A拿到了第5张票
B拿到了第5张票
C拿到了第4张票
B拿到了第3张票
A拿到了第3张票
B拿到了第2张票
C拿到了第1张票
A拿到了第2张票

案例:龟兔赛跑

image-20221216233105395
public class Race implements Runnable{

    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            //模拟兔子中途睡觉
            if (Thread.currentThread().getName().equals("兔子") && i == 50){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            //比赛结束,结束程序
            if (flag){
                break;
            }

            System.out.println(Thread.currentThread().getName() + "跑了" + i +"步");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    //判断比赛是否结束
    private boolean gameOver(int steps){
        //判断是否有胜利者
        if(winner!=null){
            return true;
        }else{
            //判断是否有人跑完了
            if(steps >= 100){
                winner = Thread.currentThread().getName();
                System.out.println("winner is " + winner);
                return true;
            }else{
                return false;
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new Race(),"兔子").start();
        new Thread(new Race(),"乌龟").start();
    }
}

3. 实现Callable接口(了解)

image-20221216234504774

实现Callable接口需要重写call方法,call需要返回值(装箱)

实现Runnable接口重写run方法,无返回值(void)

image-20221216234857377

/**
 * callable的好处
 * 1.可以定义返回值
 * 2.可以抛出异常
 */
public class TestCallable implements Callable {
    private String url;
    private String name;

    public TestCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() {
       WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("文件名为"+name+"的文件下载完毕!");
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/14.jpg", "test1.jpg");
        TestCallable t2 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/12.jpg", "test2.jpg");
        TestCallable t3 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/11.jpg", "test3.jpg");
        

        //创建执行服务:
        ExecutorService server = Executors.newFixedThreadPool(2);
        //提交执行
        Future<Boolean> result1 = server.submit(t1);
        Future<Boolean> result2 = server.submit(t2);
        Future<Boolean> result3 = server.submit(t3);
        //获取结果
        boolean rs1 = result1.get();
        boolean rs2 = result2.get();
        boolean rs3 = result3.get();
        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);
        //关闭服务
        server.shutdownNow();
    }
}


//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url, String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));

        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("io异常,downloader出现问题");
        }
    }
}

静态代理模式

image-20221217020502533

/*
静态代理模式总结:
1. 真实对象和代理对象都要实现同一个接口
2. 代理对象要代理真实对象

好处:
1. 代理对象可以做很多真实对象做不了的事情
2. 真实对象可以专注做自己的事情
 */
public class StaticProxy {
    public static void main(String[] args) {

        //对比一下线程Thread和静态代理
        //线程创建了一个实现Runnable接口的对象,类似本例中的Marry接口,然后调用start,类似本例中的HappyMarry
        new Thread( ()-> System.out.println("我爱你") ).start();
        new WeddingCompany( new You() ).HappyMarry();


//        WeddingCompany weddingCompany = new WeddingCompany(new You());
//        weddingCompany.HappyMarry();
    }
}

interface Marry{
    void HappyMarry();
}

//真实角色
class You implements Marry{

    @Override
    public void HappyMarry() {
        System.out.println("你要结婚了!");
    }
}

//代理角色,婚庆公司,帮助你结婚
class WeddingCompany implements Marry{
    //代理谁--》真实目标角色
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        //代理对象在调用真实对象之前之后都可以干一些别的事情
        before();
        target.HappyMarry();  //这就是真实对象,可以看到You类的HappyMarry是被婚庆公司调用的
        after();
    }

    private void before() {
        System.out.println("结婚前,布置现场");
    }
    private void after() {
        System.out.println("结婚后,收尾款");
    }
}

Lambda表达式

image-20221217020629804

image-20221217020642142 image-20221217020653096
/**
 * 逐渐简化的过程
 */
public class TestLambda {
    //3. 使用静态内部类
    static class B2 implements A{

        @Override
        public void test() {
            System.out.println("静态内部类");

        }
    }

    public static void main(String[] args) {
        A a = new B();//多态
        a.test();

        a = new B2();
        a.test();

        //4. 局部内部类
        class B3 implements A{
            @Override
            public void test() {
                System.out.println("局部内部类");

            }
        }

        a = new B3();
        a.test();


        //5. 匿名内部类
        a = new A(){
            @Override
            public void test() {
                System.out.println("匿名内部类");
            }
        };
        a.test();

        //6. lambda
        a = ()->{
            System.out.println("lambda");
        };
        a.test();

    }

}

//1.定义一个函数式接口
interface A {
    void test();
}

//2.使用实现类
class B implements A{

    @Override
    public void test() {
        System.out.println("外部类实现");
    }
}

再简化

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

        //lambda表示简化
        interface_A inf = (int a)->{
            System.out.println(a);
        };

        //简化1:参数类型
        inf = (a)->{
            System.out.println(a);
        };

        //简化2:简化括号 单个参数时可以简化括号
        inf = a -> {
            System.out.println(a);
        };

        //简化3: 去掉花括号,一行时才能使用
        inf = a -> System.out.println(a);

        //总结:
        //lambda表达式只有在只含一行代码时才能简化成一行,如果有多行,就用代码块包裹
        //前提接口为函数式接口
        //多个参数时,也可以去掉参数类型,要去掉就都去掉,不能去掉括号

        inf.A(2);
    }
}

interface interface_A{
    void A(int a);
}

线程状态

image-20221217020936657

image-20221217020955168

线程方法

image-20221217021042675

线程停止

建议使用外部标志flag让线程自己停下来

image-20221217021107656

线程睡眠 sleep

Sleep可以放大问题的不安全性

image-20221217021159536

//倒计时例子
public static void main(String[] args) {
    Date startTime = new Date(System.currentTimeMillis());
    while (true){
        try {
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            startTime = new Date(System.currentTimeMillis());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}

线程礼让 yield

image-20221217021227987

image-20221217021249935

Join

image-20221217021310673

观测线程状态

image-20221217021347295

public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("finish!");
        });

        System.out.println(thread.getState()); //NEW

        thread.start();
        System.out.println(thread.getState()); //RUNNABLE

        //只要线程不中止,一直打印状态
        while (thread.getState() != Thread.State.TERMINATED) {
            Thread.sleep(100);
            System.out.println(thread.getState()); //TIMED_WAITING
        }
        
        //TERMINATED
        
        //线程死亡后无法再start
//        thread.start();
    }

}

线程优先级

image-20221217022506666

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jP73oP3D-1671296934865)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217022847306.png)]

守护线程

setDaemon(true)

gc垃圾回收线程是守护线程

守护线程在程序停止的时候才停止

image-20221217023014879

image-20221217023349709

线程同步(重点)

多个线程操作同一个资源

image-20221217023640575

解决的方法就是让线程排队

image-20221217023724439

每个Object对象都拥有一把锁

image-20221217023919145

锁造成的问题 保证了安全损失了性能

image-20221217024043374

三大不安全案例

买票

package org.example.syn;

/**
 * @author QiuYuSY
 * @create 2022-12-17 2:44
 */

//不安全的买票
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        Thread t1 = new Thread(buyTicket, "A");
        Thread t2 = new Thread(buyTicket, "B");
        Thread t3 = new Thread(buyTicket, "C");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicket implements Runnable{

    private int ticketNums = 10;
    //外部循环停止标志
    boolean flag = true;
    @Override
    public void run() {
        //买票
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void buy() throws InterruptedException {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }

        //模拟延时
        Thread.sleep(100);

        //buy
        System.out.println(Thread.currentThread().getName() + "拿到第" +ticketNums-- +"张票");
    }
}

银行取钱

package org.example.syn;

/**
 * @author QiuYuSY
 * @create 2022-12-17 2:54
 */
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"账户");

        Drawing A = new Drawing(account,50,"A");
        Drawing B = new Drawing(account,100,"B");

        A.start();
        B.start();
    }
}

class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends Thread{
    Account account;
    int drawingMoney;//取多少钱
    int nowMoney;//手里的钱

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //取钱
    @Override
    public void run() {
        //判断有没有钱
        if(account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName() + "钱不够");
            return;
        }

        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //卡内余额 = 余额 - 取的钱
        account.money -= drawingMoney;
        //手里的前
        nowMoney += drawingMoney;
        System.out.println(Thread.currentThread().getName() + "余额为" + account.money);
        System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);

    }
}

B余额为-50
A余额为-50
A手里的钱50
B手里的钱100

不安全的集合

被覆盖了

public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size()); //9992
    }
}

同步方法

image-20221217031819857

弊端

只读的不需要锁,修改的才需要上锁

image-20221217031925410

同步块

  • 同步方法锁对象本身(this)
  • 同步块可以锁任何对象

image-20221217032056698

不安全案例解决

买票


//安全的买票
public class SafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        Thread t1 = new Thread(buyTicket, "A");
        Thread t2 = new Thread(buyTicket, "B");
        Thread t3 = new Thread(buyTicket, "C");

        t1.start();
        t2.start();
        t3.start();
    }
}

class BuyTicket implements Runnable{

    private int ticketNums = 10;
    //外部循环停止标志
    boolean flag = true;
    @Override
    public void run() {
        //买票
        while (flag){
            try {
                //模拟延时
                Thread.sleep(100);
                
                buy();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    //同步方法,锁的是this
    private synchronized void buy() throws InterruptedException {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        //buy
        System.out.println(Thread.currentThread().getName() + "拿到第" +ticketNums-- +"张票");
    }
}

银行取钱

需要用同步块

package org.example.syn;

/**
 * @author QiuYuSY
 * @create 2022-12-17 2:54
 */
public class SafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"账户");

        Drawing A = new Drawing(account,50,"A");
        Drawing B = new Drawing(account,100,"B");

        A.start();
        B.start();
    }
}

class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends Thread{
    Account account;
    int drawingMoney;//取多少钱
    int nowMoney;//手里的钱

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    //取钱 run方法加synchronized没用因为锁的是this,也就是银行,我们应该锁账户
    @Override
    public void run() {
        //使用同步块来锁
        synchronized (account){

            //判断有没有钱
            if(account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName() + "钱不够");
                return;
            }


            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            //卡内余额 = 余额 - 取的钱
            account.money -= drawingMoney;
            //手里的前
            nowMoney += drawingMoney;
            System.out.println(Thread.currentThread().getName() + "余额为" + account.money);
            System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);

        }

    }
}

集合

public class SafeList {
    public static void main(String[] args) throws InterruptedException {
        ArrayList<String> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());

                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

JUC集合

concurrent下

image-20221217034337340

死锁

image-20221217181935421

例子

//死锁:多个线程互相拥有对方需要的资源,形成僵持
public class DeadLock {
    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(0,"A");
        MakeUp girl2 = new MakeUp(1,"B");
        girl1.start();
        girl2.start();
    }
}

//口红
class LipStick{

}
//镜子
class Mirror{

}

class MakeUp extends Thread {
    //static保证资源只有一份
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    int choice;
    String girlName;

    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    //化妆
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            //获得口红
            synchronized (lipStick) {
                System.out.println(girlName + "获得口红的锁");
                Thread.sleep(1000);
                //一秒后想获得镜子
                synchronized (mirror) {
                    System.out.println(girlName +"获得镜子的锁");
                }
            }
        } else {
            //获得镜子
            synchronized (mirror) {
                System.out.println(girlName +"获得镜子的锁");
                Thread.sleep(2000);
                //一秒后想获得口红
                synchronized (lipStick) {
                    System.out.println(girlName +"获得口红的锁");
                }
            }
        }
    }
}

解决

把第二个synchonized拿到外面

避免互同一个代码块拥有两个以上的锁,并且互相需要对方的锁

package org.example.deadlock;

/**
 * @author QiuYuSY
 * @create 2022-12-17 18:20
 */

//死锁:多个线程互相拥有对方需要的资源,形成僵持
public class DeadLock {
    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(0,"A");
        MakeUp girl2 = new MakeUp(1,"B");
        girl1.start();
        girl2.start();
    }
}

//口红
class LipStick{

}
//镜子
class Mirror{

}

class MakeUp extends Thread {
    //static保证资源只有一份
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    int choice;
    String girlName;

    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    //化妆
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            //获得口红
            synchronized (lipStick) {
                System.out.println(girlName + "获得口红的锁");
                Thread.sleep(1000);
            }
            //一秒后想获得镜子
            synchronized (mirror) {
                System.out.println(girlName +"获得镜子的锁");
            }
        } else {
            //获得镜子
            synchronized (mirror) {
                System.out.println(girlName +"获得镜子的锁");
                Thread.sleep(2000);
            }
            //一秒后想获得口红
            synchronized (lipStick) {
                System.out.println(girlName +"获得口红的锁");
            }
        }
    }
}

image-20221217183854558

Lock(锁)

  • synchonized是隐式的锁 lock是显示的锁
  • ReentrantLock 可重入锁 实现了Lock接口

image-20221217184023331

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uwUzmuls-1671296934867)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217185547790.png)]

package org.example.syn;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author QiuYuSY
 * @create 2022-12-17 18:43
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2,"A").start();
        new Thread(testLock2,"B").start();
        new Thread(testLock2,"C").start();

    };
}

class TestLock2 implements Runnable{
    private int ticketNums = 10;
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try{
                Thread.sleep(1000);
                //加锁
                lock.lock();
                if(ticketNums > 0){
                        System.out.println(Thread.currentThread().getName()+"买到"+ticketNums--);
                }else{
                    return;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //解锁 unlock建议放在finally
                lock.unlock();
            }

        }
    }
}

synchronized和lock对比

image-20221217185646676

线程通信

生产者消费者问题

image-20221217185939663

synchronized只能实现线程同步,不能实现线程通信

image-20221217190119879

所有的对象继承自Object,所有的对象都有自己的一把锁

image-20221217190316978

解决方法1 管程法

image-20221217190435223

package org.example.notice;

import com.sun.corba.se.impl.orbutil.concurrent.Sync;

/**
 * @author QiuYuSY
 * @create 2022-12-17 19:08
 */

//测试:生产者消费者模型--》利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestPC {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }

}
//生产者
class Productor extends Thread{
    SynContainer synContainer;

    public Productor(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synContainer.push(new Chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了"+synContainer.pop().id+"只鸡");
        }
    }
}
//产品
class Chicken{
    int id; //产品编号

    public Chicken(int id) {
        this.id = id;
    }
}

//缓冲区  读写数据的方法使用synchronized把容器锁住了
class SynContainer{
    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];
    //容器计数器
    int count = 0;
    //生产者放入产品
    public synchronized void push(Chicken chicken){
        //如果容器满了,等待消费者消费
        if (count == chickens.length){
            //通知消费者消费,生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //如果容器没满,放入产品
        chickens[count] = chicken;
        count++;

        //通知消费者消费
        this.notifyAll();
    }
    //消费者消费产品
    public synchronized Chicken pop(){
        Chicken chicken = null;
        //如果容器为空,等待生产者生产
        if (count == 0){
            //通知生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        //如果容器不为空,消费者消费
        count--;
        chicken = chickens[count];

        //吃完了,通知生产者生产
        this.notifyAll();

        return chicken;
    }
}

解决方法2 信号灯

用一个标志来判断

image-20221217190641808

package org.example.notice;

/**
 * @author QiuYuSY
 * @create 2022-12-17 19:32
 */
//信号灯发,标志位解决
//生产者--演员  消费者--观众  产品--节目
//这里的观看和表演不是同步的,类似于电影,演员拍完后做成电影后观众才能看到
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}
//演员
class Player extends Thread{
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i % 2 == 0){
                this.tv.play("快乐大本营");
            }else{
                this.tv.play("抖音");

            }
        }
    }
}

//观众
class Watcher extends Thread{
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
           this.tv.watch();
        }
    }
}

//节目
class TV{
    //演员表演,观众等待
    //观众观看,演员等待
    String voice; //表演的节目
    boolean flag = true; //T时演员表演 F时演员等待

    //表演
    public synchronized void play(String voice){
        //演员线程抢到cpu
        // flag = false的时候演员等待
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println("演员表演了:" + voice);
        //通知观众来看
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }


    //观看
    public synchronized void watch(){
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("观众观看"+voice);
        this.notifyAll();
        this.flag = !this.flag;

    }
}

线程池

线程可以充分利用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5yFTtcI-1671296934868)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217195309661.png)]

image-20221217195447177

  • Runnable 使用 void execute 因为没有返回值
  • Callable 使用submit 因为有返回值
package org.example.notice;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author QiuYuSY
 * @create 2022-12-17 19:55
 */
public class TestPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);

        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        service.shutdownNow();
    }

}

class MyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

总结

package org.example.notice;

import java.util.concurrent.*;

/**
 * @author QiuYuSY
 * @create 2022-12-17 20:02
 */
public class finishTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new MyThread1().start();
        new Thread(new MyThread2()).start();

        ExecutorService service = Executors.newFixedThreadPool(1);
        Future<Integer> submit = service.submit(new MyThread3());
        Integer integer = submit.get();
        System.out.println("Callable返回值为"+integer);
        service.shutdownNow();

        //Callable的另一种实现方式
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
        new Thread(futureTask).start();
        Integer integer1 = futureTask.get();
        System.out.println("Callable返回值为"+integer1);

    }
}

//1.继承Thread类
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread类");
    }
}

//2.实现Runnable接口
class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口");
    }
}

//3.实现Callable接口
class MyThread3 implements Callable{
    @Override
    public Integer call() throws Exception {
        System.out.println("实现Callable接口");
        return 1;
    }
}

网络编程

1. 网络编程概述

1.1、概述

Java是Internet上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序。Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由JVM进行控制。并且Java实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。

计算机网络:

把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大,功能强的网络系统,从而使众多的计算机可以方便地互相传递信息,共享硬件,软件,数据信息等资源。

网络编程的目的
直接或间接地通过网络协议与其他计算机实现数据交换,进行通讯。

网络编程中有两个主要的问题

  1. 如何准确的定位网络上的一台或多台主机,定位主机上的特定的应用
    192.168.12.124:端口
  2. 找到主机后如何可靠高效地进行数据传输

网络编程 != 网页编程(web开发)
B/S 架构 和 C/S 架构

1.2、网络通信两个要素

如何实现网络中的主机互相通信?

通信双方的地址 :

  • IP 127.0.0.1
  • 端口号 3306

【比如说我们这里上课使用的就是局域网,你们连接到我的电脑,就能查看到我电脑的画面了】

(即,网络通信协议,有两套参考模型)

  • OSI 参考模型:模型过于理想化,未能在因特网上进行广泛推广!
  • TCP/IP 参考模型:TCP/IP协议,事实上的国际标准。

image-20221217212232145

image-20221217212346954

小总结:

  1. 网络编程中有两个主要的问题:
    • 如何准确的定位网络上一台或多台主机;定位主机上的特定的应用
    • 找到主机后如何可靠高效的进行数据传输
  2. 网络编程中的两个要素:
    • ip 和 端口号
    • 提供网络通信协议。 TCP/IP参考模型(应用层,传输层,网络层,物理+数据链路层),

1.3、IP

ip地址:Inet Adderss

  • 唯一的标识 internet 上的计算机 ( 通信实体 )

  • 本地回环地址(hostAddress):127.0.0.1 主机名 ( hostName ):localhost

  • IP地址分类方式一1: IPV4 IPV6

    • IPV4:4个字节组成,4个0~255。大概42亿个, 30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
    • IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号 隔开,如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
  • IP地址分类方式2:公网地址(万维网使用) 和 私有地址(局域网使用)。

    • 192.168.开头的就是私有地址,范围即为 192.168.0.0 ~ 192.168.255.255,专门为组织机构内部使用
  • 【查看 JDK 帮助文档=> InetAddress类,代表IP】

  • 特点:不便于记忆,我们常使用域名来访问:www.baidu.com

  • https://blog.kuangstudy.com/ => DNS 域名解析(150.109.117.44)=> 现在本机的hosts文件,
    判断是否有输入的域名地址,没有的话,再通过DNS服务器,找主机。
    hosts文件地址: c:\windows\system32\drivers\etc\hosts

可以看到InetAddress类是没有构造方法的(黑的),所以无法new出来

image-20221217214059131

InetAddress类

import java.net.InetAddress;
import java.net.UnknownHostException;

public class TestInetAddress {
    public static void main(String[] args) {
        try {
            //根据域名获取IP
            InetAddress address01 = InetAddress.getByName("localhost");
            System.out.println(address01);

            //获取本机地址
            InetAddress localHost = InetAddress.getLocalHost();
            System.out.println(localHost);

            InetAddress address02 = InetAddress.getByName("www.baidu.com");
            System.out.println(address02);

            System.out.println();
            System.out.println(address02.getCanonicalHostName());//180.101.49.13
            System.out.println(address02.getHostAddress());//180.101.49.13
            System.out.println(address02.getHostName());//www.baidu.com

        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}

1.4、端口号

端口号标识正在计算机上运行的进程(程序)

  • 不同的进程有不同的端口号,用来区分软件
  • 被规定为一个16位的整数 0~65535
  • TCP 和 UDP 各有 65535个端口,单个协议下端口不能冲突
  • 端口分类:
    • 公认端口: 0~1023。被预先定义的服务通信占用端口。
      • HTTP 默认端口 : 80
      • HTTPS 默认端口:443
      • FTP 默认端口: 21
      • Telnet 默认端口:23
    • 注册端口:1024~49151、分配给用户进程或应用程序。
      • tomcat 默认端口:8080
      • Mysql 默认端口:3306
      • Oracle 默认端口:1521
    • 动态、私有端口:49152~65535
netstat -ano #查看所有端口
netstat -ano|findstr "6732" #查看指定端口
tasklist|findstr "6732" #查看指定进程
# 使用任务管理器查看PID
image-20221217215906054

端口号与IP地址的组合,得出一个网络套接字:Socket,所以说一些网络编程也被称为Socket编程

InetSocketAddress类

public class TestInetAddress {
    public static void main(String[] args) {
        InetSocketAddress socketAddress = new InetSocketAddress("www.baidu.com",8080);
        System.out.println(socketAddress.getAddress());//www.baidu.com/180.101.49.14
        System.out.println(socketAddress.getHostName());//www.baidu.com
        System.out.println(socketAddress.getPort());//8080
    }
}

1.5、网络通信协议

协议:就好比我们都说的普通话,大家才能听懂我讲的,但是我们还有自己的方言!

网络通信协议:
计算机网络中实现通信必须有一些约定,即通信协议,对速率,传输代码,代码结构,传输控制步骤,
出错控制等制定标准。

问题:网路协议太复杂?
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量
控制,路由控制,如何实现如此复杂的网络协议呢?

通信协议分层的思想
在制定协议时,把复杂成份分解成一些简单的成份,再将他们复合起来。最常用的复合方式是层次方
式,即同层间可以通信,上一层调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开
发和扩展。

TCP/IP协议簇

  • 传输层协议中有两个非常重要的协议:

    • 用户传输协议 TCP (Transmission Control Protocol)传输层
    • 用户数据报协议(User Datagram Protocol)
  • TCP/IP 以其两个主要协议: 传输控制协议:TCP,和网络互联协议:IP,而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。

  • IP(Internet Protocol)协议是网络层的主要协议,支持网间互联的数据通信。

  • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层,IP层,传输层和应用层

TCP 和 UDP对比

  • TCP协议
    • 使用TCP协议前,必须建立TCP连接,形成传输数据通道;
    • 传输前,采用 三次握手 方式,点对点通信,是可靠
    • TCP协议进行通信的两个应用进程:客户端,服务端。
    • 在连接中可进行大数据量的传输
    • 传输完毕,四次挥手需要释放已建立的连接,效率低
    • 举例:打电话
image-20221217221941306 image-20221217221956954

UDP协议

  • 将数据,源,目的封装成数据包,不需要建立连接
  • 每个数据报的大小限制在64K内
  • 发送方不管对方是否准备好,接收方收到也不确认,故事不可靠的
  • 可以广播发送
  • 发送数据结束时,无需释放资源,开销小,速度快。
  • 举例:发短信,导弹(饱和攻击)

2、TCP网络编程

客户端

  1. 连接服务器socket
  2. 发送消息

服务器:

  1. 建立服务端口
  2. 等待客户端连接
  3. 读取客户的消息

2.1、案例一

需求:客户端发送信息给服务端,服务端将数据显示在控制台上。

TCPClientDemo01.java
//客户端
public class TCPClientDemo01 {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream os = null;
        try {
            //1. 要知道服务器的地址,端口号
            InetAddress serverIP = ;
            int port = 9999;
            //2.创建一个socket连接
            socket = new Socket(serverIP,port);
            //3.发送消息 IO流
            os = socket.getOutputStream();
            os.write("你好!".getBytes());
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

    }
}


TCPServerDemo01.java
//服务端
public class TCPServerDemo01 {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            //1. 要有一个服务器地址
            serverSocket = new ServerSocket(9999);

            while(true){
                //2. 等待客户端连接
                socket = serverSocket.accept();
                //3. 读取客户端的消息
                is = socket.getInputStream();

                /*回忆之前的IO流方案,弊端:存在中文,可能存在乱码。
                byte[] buffer = new byte[1024];
                int len;
                while((len = is.read(buffer))!=-1){
                    String msg = new String(buffer,0,len);
                    System.out.println(msg);
                }
                */

                //管道流
                baos = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len;
                while ((len=is.read(buffer))!=-1){
                    baos.write(buffer,0,len);
                }
                System.out.println(baos.toString());
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(baos != null){
                try {
                    baos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (is != null){
                try {
                    is.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (serverSocket != null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }
}

2.2、案例二

需求:客户端发送文件给服务器,服务端将文件保存在本地。

我们需要准备一个图片,放在项目目录下:

TCPClientDemo02.java
    
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

public class TCPClientDemo02 {
    public static void main(String[] args) {
        try {
            //1. 创建一个socket连接
            Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            //2. 创建一个输出流
            OutputStream os = socket.getOutputStream();
            //3. 读取文件,创建一个文件输入流
            FileInputStream fis = new FileInputStream(new File("test1.png"));
            //4. 写出文件
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer))!=-1){
                os.write(buffer,0,len);
            }

            //通知服务器我已经发送完毕
            socket.shutdownOutput();

            //确定服务器接收完毕,才能断开连接
            InputStream is = socket.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer2 = new byte[1024];
            int len2;
            while ((len2 = is.read(buffer2))!= -1){
                baos.write(buffer2,0,len2);
            }

            System.out.println(baos.toString());

            // 5. 关闭资源
            baos.close();
            is.close();
            fis.close();
            os.close();
            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}




TCPServerDemo02.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServerDemo02 {
    public static void main(String[] args) {
        try {
            //1. 创建服务
            ServerSocket serverSocket = new ServerSocket(9999);
            //2. 监听客户端连接  accept阻塞式监听,会一直等待客户端连接
            Socket socket = serverSocket.accept();
            //3. 获取输入流
            InputStream is = socket.getInputStream();
            //4. 文件输出
            FileOutputStream fos = new FileOutputStream(new File("receive.jpg"));
            byte[] buffer = new byte[1024];
            int len;
            while((len = is.read(buffer)) != -1){
                fos.write(buffer,0,len);
            }

            //通知客户端接收完毕了
            OutputStream os = socket.getOutputStream();
            os.write("接收完毕".getBytes());

            //关闭资源
            fos.close();
            is.close();
            socket.close();
            serverSocket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2.3、练习作业

  1. 服务端读取图片并发送给客户端,客户端保存图片到本地。
package org.example.net;


import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

public class TCPClientDemo04 {
    public static void main(String[] args) {
        try {
            //1. 创建一个socket连接
            Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            //2. 创建一个输出流
            OutputStream os = socket.getOutputStream();
            //3. 输出信息
            String picPath = "test1.png";
            os.write(picPath.getBytes());
            //通知服务器我已经发送完毕
            socket.shutdownOutput();

            //获取输入流
            InputStream is = socket.getInputStream();
            //处理流
            FileOutputStream fos = new FileOutputStream(new File("receive.png"));
            // 读取服务器发来的图片
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer))!= -1){
                fos.write(buffer,0,len);
            }

            // 5. 关闭资源
            fos.close();
            is.close();
            os.close();
            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}


package org.example.net;


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServerDemo04 {
    public static void main(String[] args) {
        try {
            //1. 创建服务
            ServerSocket serverSocket = new ServerSocket(9999);
            //2. 监听客户端连接  accept阻塞式监听,会一直等待客户端连接
            Socket socket = serverSocket.accept();
            //3. 获取输入流
            InputStream is = socket.getInputStream();
            //4. 处理流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 读取客户端发来的文本
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer))!= -1){
                baos.write(buffer,0,len);
            }
            String picPath = baos.toString();

            //获取输出流
            OutputStream os = socket.getOutputStream();
            //创建文件输入流,从服务器的本地获取文件
            FileInputStream fis = new FileInputStream(new File(picPath));
            byte[] buffer2 = new byte[1024];
            int len2;
            while ((len2 = fis.read(buffer2))!= -1){
                os.write(buffer2,0,len2);
            }

            //关闭资源
            fis.close();
            os.close();
            baos.close();
            is.close();
            socket.close();
            serverSocket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}




  1. 客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端。
package org.example.net;


import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

public class TCPClientDemo03 {
    public static void main(String[] args) {
        try {
            //1. 创建一个socket连接
            Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            //2. 创建一个输出流
            OutputStream os = socket.getOutputStream();
            //3. 输出信息
            os.write("hello world".getBytes());
            //通知服务器我已经发送完毕
            socket.shutdownOutput();

            //获取输入流
            InputStream is = socket.getInputStream();
            //处理流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 读取服务器发来的文本
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer))!= -1){
                baos.write(buffer,0,len);
            }
            String backString = baos.toString();
            System.out.println(backString);
            
            

            // 5. 关闭资源
            baos.close();
            is.close();
            os.close();
            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}




package org.example.net;


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServerDemo03 {
    public static void main(String[] args) {
        try {
            //1. 创建服务
            ServerSocket serverSocket = new ServerSocket(9999);
            //2. 监听客户端连接  accept阻塞式监听,会一直等待客户端连接
            Socket socket = serverSocket.accept();
            //3. 获取输入流
            InputStream is = socket.getInputStream();
            //4. 处理流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 读取客户端发来的文本
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer))!= -1){
                baos.write(buffer,0,len);
            }
            String readString = baos.toString();
            //转大写
            readString = readString.toUpperCase();
            //获取输出流
            OutputStream os = socket.getOutputStream();
            //输出文本
            os.write(readString.getBytes());

            //关闭资源
            os.close();
            baos.close();
            is.close();
            socket.close();
            serverSocket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2.4、 初识Tomcat服务器

客户端

  • 用网络编程自己写 C
  • 浏览器 B

服务端

  • 用网络编程自己写
  • Tomcat服务器
  1. 将提供的tomcat解压
  2. 运行bin目录下的启动文件
  3. 测试访问localhost:8080

image-20221218003759603

  1. 现在,我们在里面的 \webapps\ROOT 目录下 新建一个kuangshen.txt,编辑文件保存!

  2. 浏览器输入 http://localhost:8080/kuangshen.txt ,发现可以访问到我们客户端的资源,OK

  3. 这里,我们的浏览器,就相当于是一个客户端,而Tomcat 服务器,就相当于是服务端,具体的关
    于Tomcat的学习我们要到 JavaWeb 阶段会具体学习

3、UDP网络编程

3.1、说明

  • DatagramSocket 和 DatagramPacket 两个类实现了基于UDP协议的网络程序。
  • UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安
    全送到目的地,也不确定什么时候可以抵达。
  • DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端
    的IP地址和端口号。
  • UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接。如同发快
    递包裹一样。

UDP没有服务器和客户端的说法,分为发送端和接收端

3.2、案例一

发送端

package org.example.net.udp;

import java.io.IOException;
import java.net.*;

// 不需要连接服务
public class UDPClientDemo01 {
    public static void main(String[] args) throws IOException {
        //1. 建立一个socket
        DatagramSocket socket = new DatagramSocket();

        //2. 建个包
        String msg = "你好啊!服务器";
        // 发送给谁
        InetAddress localhost = InetAddress.getByName("localhost");
        int port = 9999;
        //数据,数据的长度起始,长度,发给谁,端口
        DatagramPacket packet = new DatagramPacket(msg.getBytes(),
                0,
                msg.getBytes().length,
                localhost,
                port);


        //3. 发送包 和TCP不同,发出去没连接也不会报错
        socket.send(packet);

        socket.close();
    }
}

接收端

package org.example.net.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

//这个不算服务端,两边相互之间都能互相往端口发
public class UDPServerDemo01 {
    public static void main(String[] args) throws IOException {
        //开发端口
        DatagramSocket socket = new DatagramSocket(9999);
        //接受数据包
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
        socket.receive(packet); //阻塞接收

        System.out.println(packet.getAddress().getHostName());
        System.out.println(packet.getAddress().getHostAddress());
        System.out.println(new String(packet.getData()));
        //close
        socket.close();
    }
}

3.3、案例二:在线咨询

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/98073.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

卷积神经网络 图像处理,卷积神经网络图片处理

1、在做图像处理时&#xff0c;如何提高识别算法的设计与效果的精度&#xff1f; 得到更多的数据 这无疑是最简单的解决办法&#xff0c;深度学习模型的强大程度取决于你带来的数据。增加验证准确性的最简单方法之一是添加更多数据。如果您没有很多训练实例&#xff0c;这将特…

AllegroBGA如何自动fanout操作指导

AllegroBGA如何自动fanout操作指导 当我们需要给BGA做fanout的时候,逐个pin去fanout是件比较麻烦的事情,下面介绍Allegro中如何自动fanout,以下图BGA为例 具体操作如下 选择Route-Create Fanout 选择Via类型,比如Via10 Via Direction BGA Quadrant Style Pin-via Space…

数字化母婴店,母婴智慧会员管理小程序

会员店想必很多小伙伴都接触过或有所耳闻&#xff0c;会员店的本质是通过好产品、好服务及好内容营造出完整且完美的用户消费体验&#xff0c;虽然纯会员店并不多&#xff0c;但如今越来越多的各行业商家意识到每个流量用户都很重要&#xff0c;需要维护好每个用户&#xff0c;…

18.Django大型项目之用户中心页面

1. 用户中心的搭建 1.1 基础搭建 这里&#xff0c;主要就是基础的页面渲染&#xff0c;使用的也是继承主模板&#xff0c;对其进行修改的方式。就直接看代码吧 1.2 上下文的应用 什么是上下文&#xff1f; 对于上下文&#xff0c;可以理解成一个公用的函数或者类 我们这里使…

Qt使用MSVC2015找不到编译器的解决办法

Qt使用MSVC2015找不到编译器的解决办法 1、问题描述 安装qt前&#xff0c;已安装vscode&#xff0c;构建套件MSVC2015 32/64bit出现红色感叹号&#xff0c;并报错构建套件中未设置编译器 2、解决办法 step1:通过windows SDK工具安装调试器。 工具下载地址&#xff1a;https:/…

244. 谜一样的牛——二分+树状数组

有 n 头奶牛&#xff0c;已知它们的身高为 1∼n 且各不相同&#xff0c;但不知道每头奶牛的具体身高。 现在这 n 头奶牛站成一列&#xff0c;已知第 i 头牛前面有 Ai 头牛比它低&#xff0c;求每头奶牛的身高。 输入格式 第 1 行&#xff1a;输入整数 n。 第 2…n 行&#x…

Linux——组管理和权限管理

Linux组 基本介绍 在Linux中的每个用户必须属于一个组&#xff0c;不能独立于组外&#xff0c;在Linux中每个文件有所有者&#xff0c;所在组&#xff0c;其他组的概念。 所有者 一般为文件的创建者&#xff0c;谁创建了该文件&#xff0c;就自然的成为该文件的所有者。 查…

构造序列(模拟构造)

请你构造一个 01 序列&#xff0c;序列需要满足以下所有要求&#xff1a; 恰好包含 n 个 0 和 m 个 1。不存在两个或两个以上的 0 连续相邻。不存在三个或三个以上的 1 连续相邻。 输入格式 共一行&#xff0c;包含两个整数 n,m。 输出格式 输出共一行&#xff0c;如果存在…

【js记录】递归进行深拷贝时对于Object遍历方法的思考【深浅拷贝+object县官属性介绍+类型判断方法+for...in+for...of】

一、 前言&#xff1a;本文使用的 类型判断方法介绍 通过手写instaceof更好理解其作用及使用 1、 typeof 判断一个实例属于哪种类型 2、 instance 判断一个实例是否属于某种类型let person function(){ } let no new person(); no instanceof person; // true3、 原型…

IT分销商未来在哪里?南京创瑞丰向复合型产品方案商转型

在一个充满不确定性的市场中&#xff0c;思变求新是企业的唯一出路。 诚如IT分销领域&#xff0c;云计算、大数据、人工智能等技术的兴起&#xff0c;加速企业数字化转型的同时&#xff0c;亦对于传统IT分销市场的未来产生了深远影响。过去“搬箱子”、打价格战的方式方法正在…

Python中文件操作(读、写、关闭)

目录 一.什么是文件 二.python文件操作 1.open&#xff08;&#xff09;打开函数 注意 mode常用的三种基础访问模式 2.读操作相关方法 read&#xff08;&#xff09;方法&#xff1a; readlines&#xff08;&#xff09;方法: readline&#xff08;&#xff09;方法 for…

Linux性能学习(1.1):CPU_CPU缓存

文章目录1 简介2 工作机制3 Cache Line参考资料&#xff1a; 1. https://www.makeuseof.com/tag/what-is-cpu-cache/ 2. https://zhuanlan.zhihu.com/p/80672073 3.CPU Cache 机制以及 Cache miss 4.性能优化方法和技巧 在日常的嵌入式开发中&#xff0c;查看一些SOC的datashe…

想学习编程,该怎么开始,需要多长时间?

想学习编程,该怎么开始&#xff0c;需要多长时间&#xff1f; 其实学编程也没有啥特别的方法&#xff0c;和我们没基础学驾照差不多。 考A照、B照还是C照&#xff0c;我们先得拎清&#xff0c;它们的用途存在一定差异 。一样的&#xff0c;学编程是学C&#xff0c;Java、Pyth…

命令行在服务器的Oracle新建、修改用户并授权

1.前提解说 由于我是使用docker拉取的Oracle,这里截图的Oracle用户是原本安装好的Oracle用户。 2.步骤 1.Linux服务器切换到Oracle su oracle 2. 进入SQL sqlplus / as sysdba 3.新建用户(一定要加“&#xff1b;”) create user 用户名 identified by 密码&#xff1b; 若是修…

Tableau可视化设计案例-02Tableau数据处理、折线图

文章目录Tableau可视化设计案例02Tableau数据处理、折线图1.数据前处理2.绘制折线图2.1 创建电影数据变化折线图2.2创建电影票房变化折线图2.3 20215年的电影数量与票房比较分析Tableau可视化设计案例 本文是Tableau的案例&#xff0c;为B站视频的笔记&#xff0c;B站视频 参考…

Java集合/泛型面试题

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;Java面试题…

作为文科生,如何才能学好一门编程语言?

学习新知识的过程总是痛苦和逆人性的&#xff0c;古往今来皆是如此&#xff0c;否则也不会有十年寒窗苦读一说。 相信此刻看这篇文章的你&#xff0c;经历的寒窗绝对不止十年。 从小学开始算起到大学毕业&#xff0c;绝大多数人起码经历了16年&#xff08;跳级的神童与学霸除…

5G无线技术基础自学系列 | 单站点验证测试

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 单站点验证工作是通过测试来进行功能性…

二级MS Office真题第8套完整解析

全国计算机等级考试二级新增预测第一套 一、文字处理 公司将于今年举办“创新产品展示说明会”&#xff0c;市场部助理小王需要将会议邀请函制作完 成&#xff0c;并寄送给相关的客户。现在&#xff0c;请你按照如下需求&#xff0c;在Word.docx文档中完成制作工作&#xff1a;…

非零基础自学Golang 第11章 文件操作 11.1 目录基本操作 11.1.1 列目录

非零基础自学Golang 文章目录非零基础自学Golang第11章 文件操作11.1 目录基本操作11.1.1 列目录第11章 文件操作 计算机文件是以硬盘为载体的信息存储集合&#xff0c;文件可以是文本、图片、程序等。在编写程序时&#xff0c;我们经常会和文件打交道&#xff0c;比如从文件读…