【Java】基础知识部分

一、数组

单个数组内存分配图

image-20210424235050443

多个数组内存分配图

image-20211018155323251

多个数组指向相同地址

image-20211018155340856

这种情况下,多个数组指向同一个地址值。

中间一行的赋值操作是将arr的地址值赋值给arr2,如果这个时候针对arr2进行操作,那么也就相当于是对arr进行操作,本质上指向的是同一个数组。所以无论操作arr还是arr2,结果上没有本质上的区别。

数组空指针异常

image-20210424235746162

如果数组被赋值为null,那么将找不到数组本身存放的堆内存地址。再次使用的时候会报错:空指针异常

二、内部类、抽象类、包装类、修饰符

内部类

在一个类中定义一个类,类中被定义的类就是内部类

内部类的访问特点
  • 内部类可以直接访问外部类的成员,包括私有

  • 外部类要访问内部类的成员,必须创建对象

public class Outer {
    private int num = 20;
    public class Inner {
        public void show() {
            System.out.println(num);
        }
    }
    private void method() {
        Inner inner = new Inner();
        inner.show();
    }
}
成员内部类

根据内部类的位置不同,可以分为两种:

  • 在类的成员位置:成员内部类
  • 在类的局部位置:局部内部类

成员内部类如何使用呢?两种方式:

一、将内部类的权限名定义为public,之后创建内部类

public class Outer {
    private int num = 20;
    public class Inner {
        public void show() {
            System.out.println(num);
        }
    }
}
public static void main(String[] args) {
    // 创建对象调用内部类方法
    Outer.Inner oi = new Outer().new Inner();
    oi.show();
}

二、如果Inner内部类的权限名不是public,则上述方法失效,那么如何调用呢?

在外部类内创建新的方法,创建内部类,调用方法;外界直接创建外部类,并调用该方法即可

public class Outer {
    private int num = 20;
    private class Inner {
        public void show() {
            System.out.println(num);
        }
    }
    public void method() {
        Inner i = new Inner();
        i.show();
    }
}
public static void main(String[] args) {
    // 创建对象调用内部类
    /*Outer.Inner oi = new Outer().new Inner();
    oi.show();*/
    Outer o = new Outer();
    o.method();
}
局部内部类

局部内部类就是在方法体中的类,所以外界是无法使用的,需要在方法中创建该局部内部类的对象,通过调用对象内部的方法使用

该类可以访问外部的成员,也可以访问方法内的局部变量

public class Outer {
    private int num = 10;
    public void method() {
        class Inner {
            int num2 = 20;
            public void show() {
                System.out.println(num);
                System.out.println(num2);
            }
        }
        // 直接在方法内部创建对象调用局部内部类的方法
        Inner i = new Inner();
        i.show();
    }
}
public static void main(String[] args) {
    Outer o = new Outer();
    o.method();
}
匿名内部类

前提:存在一个类或者一个接口,这里的类可以是具体类也可以是抽象类

格式:

new class/interface() {
    // Override method()
};

本质是***一个继承了该类或实现了该接口的子类匿名对象***

步骤一:有一个类或者接口

public interface Inter {
    void show();
}

步骤二:创建相关的类

public class Outer {
    public void method() {
        /*new Inter() {
            @Override
            public void show() {
                System.out.println("匿名内部类方法执行");
            }
        };
        这样写仅仅是个对象,下面的写法才是对象调用方法:
        new Inter() {
            @Override
            public void show() {
                System.out.println("匿名内部类方法执行");
            }
        }.show();*/

        // 由于该匿名内部类实现的是 Inter 接口,我们可以用接口类型来接受这个匿名内部类
        Inter i = new Inter() {
            @Override
            public void show() {
                System.out.println("匿名内部类方法执行");
            }
        };

        i.show();
        i.show();
    }
}

步骤三:测试

public class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        o.method();
    }
}

输出结果:

匿名内部类方法执行
匿名内部类方法执行

匿名内部类在开发中的使用

仅使用一次,创建接口操作类的对象,调用接口操作方法,方法的参数是接口

不想创建接口实现类的情况,并且只想用一次,就可以使用匿名内部类

抽象类

Java中,没有方法体的方法应该被定义为抽象方法;类中如果有抽象方法,则应该定义为抽象类

注意事项:

  • 抽象类中的方法不一定是抽象方法,但是抽象方法所属的类一定要是抽象类,抽象类中一定存在抽象方法
  • 抽象类中的子类要么是抽象类,要么重写抽象类中的所有抽象方法
  • 抽象类不能实例化,但是抽象类可以通过子类对象进行实例化,这叫抽象类多态
抽象类的成员特点

抽象类中可有成员变量、成员方法、构造方法

  • 成员变量
    • 可以是变量,也可以是常量
  • 成员方法
    • 可以有抽象方法:限定子类必须完成某些动作
    • 可以有非抽象方法:提高代码的复用性
  • 构造方法
    • 有构造方法,但是不能实例化
    • 抽象方法的实例化是通过子类的对象进行实例化的,子类对象对于父类数据的初始化要使用到这些构造方法
public abstract class Animal {
    // 抽象类中可以包含成员变量
    private int age = 20;
    private final String city = "北京";

    // 因为抽象类是通过子类对象进行实例化的,所以子类对象在使用构造方法创建时,
    // 会隐式的调用父类的构造方法,也就是抽象类的构造方法
    public Animal() {}

    public Animal(int age) {
        this.age = age;
    }

    public void show() {
        age = 40;
        System.out.println(age);
        System.out.println(city);
    }

    /*public void eat() {
        System.out.println("吃东西");
    }*/
    // 抽象方法
    public abstract void eat();
    // 抽象类中可以有具体的方法
    public void sleep() {
        System.out.println("睡觉");
    }
}

包装类

image-20210406145151828

基本数据类型使用虽然非常方便,但是没有对应的方法来操作这些数据。所以我们可以使用包装类将这些基本数据类型进行一定的封装,把基本类型的数据包装起来,这就是包装类。

在包装类中可以定义一些方法,用来操作基本类型的数据。

装箱与拆箱

image-20210406150512203

修饰符

Java中的修饰符分为两大类:权限修饰符、状态修饰符

权限修饰符

image-20210509204638596

权限修饰符,修饰的是访问的权限,指的是在同一个module中的不同类中的访问权限

状态修饰符
final(最终态)

可以修饰成员方法、成员变量、类

  • final修饰方法,表明该方法是最终方法,不能被重写
  • final修饰变量,表明该变量是最终变量,不能被再次赋值
  • final修饰类,表明该类是最终类,不能被继承

final修饰局部变量:

  • final修饰基本数据类型变量,变量的数据值不能发生改变
  • final修饰引用数据类型变量,变量的地址值不能发生改变,但是地址里的内容是可以发生改变的
static(静态)

可以修饰成员方法、成员变量

  • 被类的所有对象共享——这也是我们判断是否使用static关键字的条件
  • 可以通过类名.变量名调用(也可以使用对象名调用),推荐使用类名调用
  • 静态成员方法只能访问静态成员

三、继承、多态

继承

继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法

继承的利弊
  • 优点:
    • 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
    • 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
  • 缺点:
    • 继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时,子类实现也不得不跟着变化,削弱了子类的独立性
继承使用的情况

什么时候使用继承?

当类A是类B的”一种/一个“时,就可以使用继承关系。

继承中成员变量访问
  • 在子类方法中访问变量
    • 子类局部范围找
    • 子类成员范围找
    • 父类成员范围找
    • 如果都没有就报错(不考虑父类的父类)
super与this关键字的使用
  • super:代表父类存储空间的标识(可以理解为父类对象引用)
  • this:代表本类对象的引用

image-20210509194539063

继承中成员方法的访问

继承中的成员方法的访问:(子类访问成员方法)

  • 子类成员范围找
  • 父类成员范围找
  • 如果找不到就报错(不考虑父类的父类)
public static void main(String[] args) {
    Zi zi = new Zi();
    // 调用子类成员方法
    zi.method();
    // 调用子类和父类中的重名无参方法
    // 真正调用的是子类中的同名方法
    zi.show();
    // 调用父类中的方法,需要在子类中的同名方法中添加 super.show();
}
继承中构造方法的访问特点

继承中,关于构造方法的访问:

  • 子类中所有的构造方法都会默认访问父类中的无参构造方法

  • 原因:子类继承自父类,在子类调用父类时可能会用到父类的数据,所以在子类进行初始化的时候需要先对父类进行初始化操作

  • 因为子类会继承父类的数据,可能还会使用父类的数据。所以在子类初始化之前需要先完成父类数据的初始化

  • 每一个子类构造方法的第一句默认都是super();

如果父类中没有无参构造方法,只有带参构造方法,解决方法:

  • 通过使用super关键字显式的调用父类的带参构造方法
  • 在父类中自己提供一个无参构造方法(推荐使用)
public static void main(String[] args) {
    /*
    * 父类无参构造方法被调用
    * 子类无参构造方法被调用
    * */
    Zi z1 = new Zi();
    /*
    * 父类无参构造方法被调用
    * 子类带参构造方法被调用
    * */
    Zi z2 = new Zi(20);
}
super中的内存图

image-20210509201403215

方法重写

子类中出现了和父类中一样的方法声明

当子类中需要父类的功能,而功能主体子类有自己特有内容,可以重写父类中的方法。这样既沿袭了父类中的功能,又定义了子类特有的功能

方法重写时最好添加@Override注解,可以帮忙检查方法重写的方法声明是否正确

注意事项

  • 父类中的私有方法子类不能重写(父类中的私有成员子类是不能被继承的)
  • 子类重写父类方法,子类的访问权限不能更低。例如:父类成员方法为默认,子类重写方法权限为默认或比默认更高(protected,public)
    • 方法访问权限:public > protected > 默认 > private
继承的注意事项

Java中的继承只支持单继承,不支持多继承(一个类继承自多个类,不允许)

继承支持多级继承,子类继承自父类,父类继承自父类的父类(爷爷类)这样的继承是合法的

多态

多态定义的时候:左父右子

多态中成员访问

成员变量:编译看左边,执行看左边

成员方法:编译看左边,执行看右边

成员方法和成员变量执行不同的原因:因为成员方法有重写,而成员变量没有

底层上的解释就是成员变量属于前期绑定(静态绑定,程序编译期的绑定),成员方法属于后期绑定(动态绑定,程序运行期的绑定)。

重载属于前期绑定,重写属于后期绑定。

也就是说:多态的编译是否能通过,要看父类中是否有相关的变量和方法;执行则要看是变量还是方法

多态的利弊
  • 多态的好处:提高了程序的扩展性

  • 多态的弊端:不能使用子类的特有功能

  • 定义多态方法的时候,使用父类型作为参数,使用具体的子类型进行操作

实际使用如下:

①创建Animal类

public class Animal {
    public void eat() {
        System.out.println("动物吃东西");
    }
}

②创建Dog类和Cat类

public class Cat extends Animal{
    public void eat() {
        System.out.println("猫吃老鼠");
    }
}
public class Dog extends Animal{
    public void eat() {
        System.out.println("狗吃骨头");
    }

    public void gatekeeper() {
        System.out.println("狗看门");
    }
}

③创建测试主类

public class AnimalDemo {
    public static void main(String[] args) {
        Animal a = new Cat();
        a.eat();

        Animal b = new Dog();
        // b.gatekeeper();
        b.eat();
    }
}

这个时候我们调用gatekeeper方法则会报错,因为gatekeeper方法是Dog类独有的方法。多态的弊端此时体现出来了:因为在Animal父类中没有定义gatekeeper方法,那么在使用多态的时候就不能调用到子类的独有方法。

解决方法:①在Animal类中添加Dog的特有方法,那这样Cat类也能够调用gatekeeper方法,本质上二者相悖了。

public void gatekeeper() {
    System.out.println("动物看家护院");
}

所以说,多态的弊端就是不能调用子类的特有方法。

②向下转型,将Animal类定义的时候的b对象(Dog)转型为Dog本身的类型。这样转型之后其实也与多态定义的时候的方法不相符,违背了多态本身的定义。

((Dog) b).gatekeeper();

四、接口、泛型

接口Interface

接口就是一种公共的规范标准,Java中的接口更多体现在对行为的抽象

接口使用interface关键字来创建;接口的使用是通过类来实现该接口实现的

public interface Jumping {
    public abstract void jump();
}

接口不能实例化,要想使用需要通过一个类来实现该接口,通过实现类对象来实例化(与抽象类在这一方面类似),叫做接口多态

接口的成员特点
  • 成员变量
    • 只能是常量,默认由public static final修饰,不能进行二次赋值
  • 成员方法
    • 只能是抽象方法,默认由public abstract修饰,不能是非抽象方法
  • 构造方法
    • 接口中没有构造方法,因为接口主要是对行为进行抽象,没有具体存在
    • 一个类如果没有父类,则默认继承自Object类
public interface Inter {
    public int num = 20; // 接口中的成员变量默认是被 static final 修饰
    public final int num2 = 30;
    public static final int num3 = 40;

    public abstract void method();
    void show();

    /* 接口中不能有构造方法和非抽象方法的
    public Inter() {}
    public void show() {}*/
}

泛型Generic

image-20210424221805237

泛型的使用可以使一些集合的使用中可能出现的类型错误,由运行期错误转换为编译期错误,编码的时候更加安全。

①把运行期间的问题提前到了编译期

②避免了强制类型转换

泛型类

image-20210424222721257

使用步骤:

①创建泛型类Generic类

public class Generic<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

②使用泛型类

public class GenericDemo {

    public static void main(String[] args) {
        Student student = new Student();
        student.setStuName("张三");
        student.setStuAge(18);
        System.out.println(student.getStuName());
        System.out.println(student.getStuAge());

        Generic<String> g1 = new Generic<String>();
        g1.setT("李四");
        System.out.println(g1.getT());

        Generic<Integer> g2 = new Generic<Integer>();
        g2.setT(20);
        System.out.println(g2.getT());
    }

}

对比普通类(student),泛型类在使用的时候更加简便,不需要过多的创建set和get方法

泛型方法

在上述的例子中,我们使用泛型方法需要多次创建新的对象,使用中还是比较繁琐。为了能够创建一次对象,多次调用不同的参数的同一个方法,我们可以使用泛型方法。

定义格式如下

public class GenericFunction {
    public <T> void show(T t) {
        System.out.println(t);
    }
}

使用方法

GenericFunction g = new GenericFunction();
g.show("雨下一整晚Real");
g.show(20);
g.show(true);

打印输出结果如下所示:

雨下一整晚Real
20
true

泛型接口

我们定义一个泛型接口

public interface GeneticInterface<T> {
    void show(T t);
}

定义接口的实现类

public class GenericInterfaceImpl<T> implements GeneticInterface<T>{
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}

测试运行类

GeneticInterface<String> geneticInterface1 = new GenericInterfaceImpl<String>();
geneticInterface1.show("Real");

GeneticInterface<Integer> geneticInterface2 = new GenericInterfaceImpl<Integer>();
geneticInterface2.show(21);

GeneticInterface<Boolean> geneticInterface3 = new GenericInterfaceImpl<Boolean>();
geneticInterface3.show(true);

运行结果如下

Real
21
true

类型通配符

image-20210424230018722

public class GenericDemo {
    public static void main(String[] args) {
        List<?> list1 = new ArrayList<Object>();
        List<?> list2 = new ArrayList<Number>();
        List<?> list3 = new ArrayList<Integer>();
        /*这三个类是继承关系,按照继承顺序编写的*/
        System.out.println("--------");

        /*类型通配符上限*/
        // List<? extends Number> list4 = new ArrayList<Object>();
        List<? extends Number> list5 = new ArrayList<Number>();
        List<? extends Integer> list6 = new ArrayList<Integer>();

        /*类型通配符下限*/
        List<? super Number> list7 = new ArrayList<Object>();
        List<? super Number> list8 = new ArrayList<Number>();
        // List<? super Number> list9 = new ArrayList<Integer>();

    }
}

在上述的代码中,添加注释的行是错误的。根据上限和下限的定义,我们可以得出super和extends的使用。

可变参数

要想实现多个数字之和,这种方法的实现,需要用到可变参数

如果为每一个数量的数求和编写一个方法,那么工作量将会变得非常大。这个时候我们就可以用到可变参数

使用如下:

public static void main(String[] args) {
    System.out.println(sum(10, 20));
    System.out.println(sum(10, 20, 30));
    System.out.println(sum(10, 20, 30, 40));
}

static int sum(int... a) {
    int sum = 0;
    for (int i : a) {
        sum += i;
    }
    return sum;
}

其中,a是一个数组类型的数据。我们求和的时候直接遍历数组求和即可。

如果sum方法有多个参数,那么可变参数应该放在后面

image-20210424231843121

如果调换二者的顺序,则不能通过编译。

可变参数的使用

image-20210424233045153

public static void main(String[] args) {
    List<String> list = Arrays.asList("Hello", "World", "java");
    // UnsupportedOperationException
    // list.add("java EE");
    // list.remove("java");
    list.set(2, "java EE");
    System.out.println(list);

    List<String> stringList = List.of("Hello", "World", "java");
    // UnsupportedOperationException
    // stringList.add("java EE");
    // stringList.remove("java");
    // stringList.set(2, "java EE");
    System.out.println(stringList);

    // set集合不允许有重复元素
    Set<String> set = Set.of("Hello", "World", "java");
    // UnsupportedOperationException
    // set.add("java EE");
    // set.remove("java");
    System.out.println(set);
}

注释掉的部分是不支持的内容,不允许的部分。

五、集合

提供一种存储空间可变的存储模型,存储的数据容量可以随时发生改变

集合分为单列集合(Collection:单值形式)和双列集合(Map:K-V形式,键值对)

集合(黑体的是接口,其余的为实现类)

  • Collection 单列集合
    • List 元素可重复
      • ArrayList
      • LinkedList
    • Set 元素不可重复
      • HashSet
      • TreeSet
  • Map 双列集合
    • HashMap

Collection

Collection集合概述

  • 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素
  • JDK不提供此接口的任何直接实现,它提供更具体的子接口(如Set和List) 实现

创建Collection集合的对象

  • 多态的方式
  • 具体的实现类,如ArrayList
public static void main(String[] args) {
    Collection<String> collection = new ArrayList<String>();
    collection.add("Hello");
    collection.add("World");
    collection.add("java");
    System.out.println(collection);
}

输出的结果是:[Hello, World, java]

说明ArrayList实现类中重写了toString方法

常用方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMHSvdQq-1639662744842)(https://i.loli.net/2021/04/25/BzK4NoIw5sya7bL.png)]

public static void main(String[] args) {
    Collection<String> collection = new ArrayList<String>();
    // 添加对应元素
    collection.add("Hello");
    // 移除指定元素
    collection.remove("Hello");
    collection.add("World");
    // 清除所有元素
    collection.clear();
    collection.add("java EE");
    // 是否包含特定元素
    System.out.println(collection.contains("java EE"));
    // 判断集合是否为空
    System.out.println(collection.isEmpty());
    // 返回集合的元素个数
    System.out.println(collection.size());
    System.out.println(collection);
}
集合中元素的遍历

利用迭代器Iterator,集合的专用遍历方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0UofVd4k-1639662744842)(https://i.loli.net/2021/04/25/4DdeiuUkxHIrbJa.png)]

public static void main(String[] args) {
    Collection<String> collection = new ArrayList<String>();
    collection.add("Hello");
    collection.add("World");
    collection.add("java");
    System.out.println(collection);

    // 获得迭代器的方法
    Iterator<String> iterator = collection.iterator();
    // 获取元素的方法
    System.out.println(iterator.next());
    /*System.out.println(iterator.next());
    System.out.println(iterator.next());
    System.out.println(iterator.next());
    System.out.println(iterator.next());*/
    // 正确的遍历方法,使用 hasNext 方法判断是否有下一个元素再进行访问
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}
集合的使用步骤

image-20210425014246288

List

image-20210425014446543

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("Hello");
    list.add("World");
    list.add("java");
    list.add("World");
    System.out.println(list);
    // 采用迭代器的方式进行遍历列表
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
    // 采用for循环的方式遍历列表
    for (String s : list) {
        System.out.println(s);
    }
}

可重复,遍历方式有两种

特有方法

image-20210425015116983

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("Hello");
    list.add("World");
    // 在集合中的指定位置插入指定的元素
    list.add(2, "java");
    // 在集合中删除指定索引处的元素
    list.remove(2);
    // 修改指定位置处的元素
    System.out.println(list.set(1, "java EE"));
    // 返回指定位置的元素
    System.out.println(list.get(1));
    System.out.println(list);
}
并发修改异常

需求:在集合中遍历元素,如果发现有World元素存在,我们就往集合中添加新的元素

public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        list.add("World");
        list.add("java");
        // ConcurrentModificationException : 并发修改异常
        /*Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()) {
            String s = iterator.next();
            if ("World".equals(s)) {
                list.add("java EE");
            }
        }*/
        // ConcurrentModificationException
        /*for (String s : list) {
            if ("World".equals(s)) {
                System.out.println(list.add("java EE"));;
            }
        }*/

        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            if ("World".equals(s)) {
                System.out.println(list.add("java EE"));;
            }
        }

        System.out.println(list);
    }

注释部分运行结果:

Exception in thread “main” java.util.ConcurrentModificationException
at java.base/java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 1012 ) a t j a v a . b a s e / j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:1012) at java.base/java.util.ArrayList Itr.checkForComodification(ArrayList.java:1012)atjava.base/java.util.ArrayListItr.next(ArrayList.java:966)
at itheima_04.ListDemo01.main(ListDemo01.java:16)

这时候会报错,是因为并发修改异常。在源代码中查看,我们可以看到在遍历集合的时候,会出现两个参数

  • modCount 实际修改次数
  • expectedModCount 预期修改次数

当这两个值不相等的时候,会抛出并发修改异常。add方法运行时候对modCount做了+1操作,但是这时候expectedModCount并没有执行+1操作。下次访问的时候,Itr类中存在的判断两者相等操作执行,得出两者的不相等,抛出异常。

利用ArrayList修改元素,使用迭代器进行遍历的时候,会使得预期修改次数得不到该有的变化。

迭代的时候,之所以设置两个参数,不允许添加元素,是因为如果一直在迭代的时候添加元素,可能会造成迭代永远不会结束的情况。

可以看出,foreach循环(增强for循环)在这里本质上也是使用了迭代器运行。(查看语法糖可以得知,底层也是利用了迭代器进行实现的)

用for循环实现的时候,则没有出现异常情况,可以正常运行。

true
[Hello, World, java, java EE]

  • 并发修改异常:ConcurrentModificationException
  • 产生原因:
    • 迭代器遍历的过程中,通过集合对象修改了集合中元素的长度,造成了迭代器获取元素中判断预期修改值和实际修改值不一致
  • 解决方案
    • 用for循环遍历,然后用集合对象做对应的操作即可

迭代器的本质理解:

迭代器的作用是将集合中的元素按照顺序遍历访问,依次返回第0号元素、第1号元素等。如果这个时候访问到第3号元素的时候,往第0号元素的位置添加元素,那么后面的所有元素都将往后移位置,也就是第3号元素会出现重复访问,造成严重后果。

Java认为,在迭代的时候,容器大小应该保持不变。这也是为什么会在迭代器中设置两个变量modCount以及expectedModCount是否相等的判断,用这两个元素进行比较,如果发生添加或删除操作,那么modCount就会+1,而期望值却没有发生改变,导致两者数值不一致,从而抛出异常。

之所以利用普通for循环调用get方法能够实现,是因为在get方法中并没有添加二者的判断。modCount只存在于add或者remove方法中,并不存在get方法中,也没有必要再get方法中添加二者的数值相等的判断。

ListIterator

列表迭代器:通过list集合的listiterator方法得到,是list集合的特有迭代器。

相比父类Iterator,它可以随意设置遍历的顺序,并且能够在迭代的时候进行修改。

向后遍历 hsaNext

向前遍历 hasPrevious

image-20210425150053550

这里面之所以可以遍历时进行修改,是因为在源码中,将modCount赋值给了expectedModCount了,不会造成二者的不相等。

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("Hello");
    list.add("World");
    list.add("java");

    // 获取list迭代器
    ListIterator<String> stringListIterator = list.listIterator();
    // 向后遍历
    while (stringListIterator.hasNext()) {
        String s = stringListIterator.next();
        if ("World".equals(s)) {
            stringListIterator.add("java EE");
        }
    }
    System.out.println(list);
    // 向前遍历
    while (stringListIterator.hasPrevious()) {
        System.out.println(stringListIterator.previous());
    }
}
增强for循环(for each)

增强for:简化数组和Collection集合的遍历

  • 实现Iterable接口的类允许其对象成为增强型 for语句的目标
  • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器

增强for的格式

  • 格式:

    for(元素数据类型变量名 : 数组或者Collection集合) {
    	//在此处使用变量即可,该变量就是元素
    }
    

编写代码如下所示:

public static void main(String[] args) {
    int[] arr = {1, 2, 3, 4, 5};

    for (int i : arr) {
        System.out.println(i);
    }

    String[] strings = {"Hello", "World", "java"};
    for (String string : strings) {
        System.out.println(string);
    }

    List<String> list = new ArrayList<String>();
    list.add("Hello");
    list.add("World");
    list.add("java");
    // foreach内部是一个Iterator迭代器
    for (String s : list) {
        System.out.println(s);
        if (s.equals("World")) {
            list.add("java EE");
        }
    }
}

运行结果如下所示,在Iterator迭代器中,修改集合的数量操作,报出并发修改异常

image-20210427014507547

List常用子类

ArrayList:底层数据结构是数组,查询快,增删慢

LinkedList:底层数据结构是链表,查询慢,增删快

// 用 ArrayList 和 LinkedList 完成存储字符串并遍历
public static void main(String[] args) {
    // 创建集合对象
    ArrayList<String> arrayList = new ArrayList<String>();
    arrayList.add("Hello");
    arrayList.add("World");
    arrayList.add("java");
    for (String s : arrayList) {
        System.out.println(s);
    }
    Iterator<String> iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
    for (int i = 0; i < arrayList.size(); i++) {
        System.out.println(arrayList.get(i));
    }

    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("Hello");
    linkedList.add("World");
    linkedList.add("java EE");
    for (String s : linkedList) {
        System.out.println(s);
    }
    Iterator<String> stringIterator = linkedList.iterator();
    while (stringIterator.hasNext()) {
        System.out.println(stringIterator.next());
    }
    for (int i = 0; i < linkedList.size(); i++) {
        System.out.println(linkedList.get(i));
    }
}
LinkedList集合的特有功能

image-20210427212018137

public static void main(String[] args) {
    // 测试 LinkedList 集合特有功能
    LinkedList<String> list = new LinkedList<String>();
    list.add("Hello");
    list.add("World");
    list.add("java");

    // 经测试,LinkedList 集合重写了toString方法
    System.out.println(list.toString());
    list.addFirst("First");
    list.addLast("Last");
    System.out.println(list.toString());
    list.removeFirst();
    list.removeLast();
    System.out.println(list.toString());
    System.out.println(list.getFirst());
    System.out.println(list.getLast());
}

Set

①不包含重复元素的集合

②没有带索引的方法,所以不能用普通for循环进行遍历

/*
* HashSet : 对集合的迭代顺序不做任何保证
* 无须且不重复,不能添加重复的元素(添加之后无效,不报错)
* */
public static void main(String[] args) {
    Set<String> set = new HashSet<String>();
    set.add("Hello");
    set.add("World");
    set.add("java");
    // set.add("java");
    System.out.println(set.toString());
    for (String s : set) {
        System.out.println(s);
    }
    Iterator<String> iterator = set.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}
哈希值

哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值

Object类中有一个方法可以获取对象的hash值

  • 同一个对象多次调用hashCode,获得的hash值是相同的
  • 通过重写hashCode方法,可以让不同对象获得的hashCode值相同
  • 但是在特殊情况下,有可能不同的对象在不重写方法的情况下还是会出现相同的hashCode值
public static void main(String[] args) {
    Student student = new Student("张三", 20);
    // 同一个对象多次调用HashCode方法,输出的hash值是一样的
    System.out.println(student.hashCode()); // 189568618
    System.out.println(student.hashCode()); // 189568618
    Student student2 = new Student("张三", 20);
    System.out.println(student2.hashCode()); // 793589513
    System.out.println(student2.hashCode()); // 793589513
    // 通过在类中重写hashCode方法,可以实现不同对象返回相同的hash值
    System.out.println("Hello".hashCode()); // 69609650
    System.out.println("World".hashCode()); // 83766130
    System.out.println("重地".hashCode()); // 1179395
    System.out.println("通话".hashCode()); // 1179395
}
HashSet保证元素唯一性分析

image-20210427221941612

HashSet<String> hashSet = new HashSet<String>();
hashSet.add("Hello");
hashSet.add("World");
hashSet.add("java");
System.out.println(hashSet);

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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

// hash值是根据元素的hashCode()方法得到的
// hash值和元素的hashCode方法相关

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果哈希表未初始化,就对哈希表进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 根据对象的hash值计算对象的存储位置,如果该位置没有元素,则存储元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        /*
        1. 将存入的元素和以前的元素比较hash值
           如果hash值不同,则表示存入的元素为新元素(HashSet中没有的元素)
           会继续向下执行,将元素添加进hashSet中
        2. 如果hash值相同,则会调用对象的equals方法进行比较
               如果返回false,会继续向下执行,把元素添加到集合
               如果返回true,说明元素重复,不存储
        */
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        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;
}
哈希表

哈希表是一种特殊的数据结构,通过链表+数组的方式实现

在jdk 8之后,对哈希表底层做了优化

image-20210427222307522

关于存储对象中,使用哈希表存储不同的对象:

/*
* 要求用 HashSet存储集合,并且保证集合中元素的唯一性
* */
public static void main(String[] args) {
    // 创建 HashSet 的 Student 集合对象
    HashSet<Student> hashSet = new HashSet<Student>();
    // 创建 Student 对象
    Student student1 = new Student("张三", 18);
    Student student2 = new Student("李四", 19);
    Student student3 = new Student("王五", 20);
    Student student4 = new Student("王五", 20);
    // 把学生对象添加到 HashSet 中
    hashSet.add(student1);
    hashSet.add(student2);
    hashSet.add(student3);
    hashSet.add(student4);
    for (Student student : hashSet) {
        System.out.println(student.toString());
    }
}

这样的情况下,直接使用hashSet会使得student3和student4对象同时均添加进集合中

为了解决这样的情况,我们在Student类中重写equal()和hashCode()方法

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(stuName, student.stuName);
}

@Override
public int hashCode() {
    return Objects.hash(stuName, age);
}
LinkedHashSet集合

image-20210427223542289

public static void main(String[] args) {
    LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>();
    linkedHashSet.add("Hello");
    linkedHashSet.add("World");
    linkedHashSet.add("World");
    linkedHashSet.add("java");
    for (String s : linkedHashSet) {
        System.out.println(s);
    }
}

添加元素的时候,不会出现重复的元素,由哈希表保证元素的唯一性,由链表保证元素的有序

TreeSet集合

image-20210427224129129

public static void main(String[] args) {
    TreeSet<Integer> treeSet = new TreeSet<Integer>();
    treeSet.add(10);
    treeSet.add(50);
    treeSet.add(30);
    treeSet.add(40);
    treeSet.add(20);
    treeSet.add(30);
    for (Integer integer : treeSet) {
        System.out.println(integer);
    }
}

输出的结果是10 20 30 40 50其中不包含重复元素,而且按照自然排序进行输出

自然排序Comparable的使用

将学生存储金TreeSet集合中,使用无参构造对学生对象根据年龄进行排序

年龄相同时,按照字母顺序进行排序

/*
* 将学生按照年龄排序,如果年龄一样,按照字母顺序排序
* */
public static void main(String[] args) {
    TreeSet<Student> treeSet = new TreeSet<Student>();
    Student student1 = new Student("张三", 17);
    Student student2 = new Student("李四", 20);
    Student student3 = new Student("王五", 18);
    Student student4 = new Student("赵六", 18);
    treeSet.add(student1);
    treeSet.add(student2);
    treeSet.add(student3);
    treeSet.add(student4);
    for (Student student : treeSet) {
        System.out.println(student);
    }
}

在Student类中,重写compareTo方法,并且实现Comparable接口

public class Student implements Comparable<Student>{
    @Override
    public int compareTo(Student o) {
        /*return 0; // 认为元素是重复的
        return 1; // 将元素按照正序输出
        return -1; // 将元素按照反序输出*/
        // 按照年龄从小到大排序
        int i = this.age - o.age; // 按照升序排列
        // int i = o.age - this.age; // 按照降序排列
        // 按照字母排序(年龄一样的情况下)
        int i1 = i == 0 ? this.stuName.compareTo(o.stuName) : i;
        return i1;
    }
}

结论:

  • 用TreeSet存储自定义对象集合的时候,无参构造使用的是自然排序对元素进行排序的
  • 自然排序,就是让元素所属的类实现Comparable接口,并且重写compareTo(Object o)方法
  • 重写方法时,一定要按照规定的要求的主要条件和次要条件来编写
比较器Comparator的使用
/*
 * 将学生按照年龄排序,如果年龄一样,按照字母顺序排序
 * */
public static void main(String[] args) {
    TreeSet<Student> treeSet = new TreeSet<Student>(new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
            int num = s1.getAge() - s2.getAge();
            int num2 = num == 0 ? s1.getStuName().compareTo(s2.getStuName()) : num;
            return num2;
        }
    });
    Student student1 = new Student("张三", 17);
    Student student2 = new Student("李四", 20);
    Student student3 = new Student("王五", 18);
    Student student4 = new Student("赵六", 18);
    treeSet.add(student1);
    treeSet.add(student2);
    treeSet.add(student3);
    treeSet.add(student4);
    for (Student student : treeSet) {
        System.out.println(student);
    }
}

结论:

  • 用TreeSet存储自定义对象集合的时候,带参方法使用的是比较器排序对元素进行排序的
  • 自然排序,就是让集合构造方法接收Comparator的实现类,并且重写compareTo(Object o)方法
  • 重写方法时,一定要按照规定的要求的主要条件和次要条件来编写

案例:用TreeSet集合类存储多个学生信息,并且按照总分成绩进行排序(语文成绩+数学成绩)

①创建Student类

public class Student {

    private String name;
    private int chinese;
    private int math;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", chinese=" + chinese +
                ", math=" + math + ", total=" +
                (math + chinese) + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return chinese == student.chinese && math == student.math && Objects.equals(name, student.name);
    }

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

    public Student(String name, int chinese, int math) {
        this.name = name;
        this.chinese = chinese;
        this.math = math;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getChinese() {
        return chinese;
    }

    public void setChinese(int chinese) {
        this.chinese = chinese;
    }

    public int getMath() {
        return math;
    }

    public void setMath(int math) {
        this.math = math;
    }
}

②编写主程序

public static void main(String[] args) {
    TreeSet<Student> students = new TreeSet<Student>(new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
            int num = s1.getChinese() + s1.getMath() - s2.getChinese() - s2.getMath();
            int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
            return -num2;
        }
    });
    Student student1 = new Student("张三", 60, 80);
    Student student2 = new Student("李四", 70, 60);
    Student student3 = new Student("王五", 85, 75);
    Student student4 = new Student("赵六", 90, 65);
    students.add(student1);
    students.add(student2);
    students.add(student3);
    students.add(student4);
    for (Student student : students) {
        System.out.println(student);
    }
}

输出结果实现了根据总分降序排列的需求,并且成绩相同的情况下根据姓名进行了排序。

Student{name=‘王五’, chinese=85, math=75, total=160}
Student{name=‘赵六’, chinese=90, math=65, total=155}
Student{name=‘张三’, chinese=60, math=80, total=140}
Student{name=‘李四’, chinese=70, math=60, total=130}

Map

interface Map<K,V>

将键映射到值的对象,不能包含重复的键,每个键可以映射到最多一个值

举例:学生姓名和学号的关系,学号就是键,值就是姓名

创建Map采用多态的方式,我们选用的是hashMap

public static void main(String[] args) {
    Map<String, String> map = new java.util.HashMap<String, String>();
    map.put("18408000101", "张三");
    map.put("18408000102", "李四");
    map.put("18408000103", "王五");
    // 键重复的时候,会使用新添加的值覆盖掉之前的值
    map.put("18408000103", "赵六");
    System.out.println(map);
    // {18408000101=张三, 18408000102=李四, 18408000103=赵六}
}
Map集合的基本功能

image-20210428104144495

public static void main(String[] args) {
    // 创建集合元素
    Map<Integer, String> map = new HashMap<Integer, String>();
    map.put(1, "张三");
    map.put(2, "李四");
    map.put(3, "王五");
    map.put(4, "王五");
    // 返回的是键所对应的值
    System.out.println(map.remove(1));
    System.out.println(map);
    // 移除所有键值对数据
    /*map.clear();
    System.out.println(map);*/
    // 是否包含键
    System.out.println(map.containsKey(2));
    // 是否包含数据
    System.out.println(map.containsValue("王五"));
    // 是否为空
    System.out.println(map.isEmpty());
    // 输出长度
    System.out.println(map.size());
}
Map集合的获取功能

image-20210428105302542

public static void main(String[] args) {
    // 创建集合元素
    Map<Integer, String> map = new HashMap<Integer, String>();
    map.put(1, "张三");
    map.put(2, "李四");
    map.put(3, "王五");
    // 根据键返回值
    System.out.println(map.get(1));;
    // 返回所有键的集合
    Set<Integer> integers = map.keySet();
    System.out.println(integers);
    // 返回所有值的集合
    Collection<String> values = map.values();
    System.out.println(values);
}
Map集合的遍历
public static void main(String[] args) {
    // 创建集合元素
    Map<Integer, String> map = new HashMap<Integer, String>();
    map.put(1, "张三");
    map.put(2, "李四");
    map.put(3, "王五");

    // 1. 获取所有键的集合
    Set<Integer> integers = map.keySet();
    for (Integer integer : integers) {
        System.out.println(integer + "," + map.get(integer));
    }
    // 2. 利用entrySet获取对象集合
    Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
    for (Map.Entry<Integer, String> entry : entrySet) {
        System.out.println(entry.getKey() + "," + entry.getValue());
    }
}

遍历Map有两种方式:获取所有键的集合,根据键找到对应的值;利用entrySet获取到Map中的每一对元素,之后调用getKey和getValue方法得到键值对的值

HashMap存储学生对象并遍历
public static void main(String[] args) {
    Map<String, Student> map = new HashMap<String, Student>();
    Student student1 = new Student("张三", 18);
    Student student2 = new Student("李四", 19);
    Student student3 = new Student("王五", 20);
    map.put("001", student1);
    map.put("002", student2);
    map.put("003", student3);
    Set<Map.Entry<String, Student>> entries = map.entrySet();
    for (Map.Entry<String, Student> entry : entries) {
        System.out.println(entry.getKey() + entry.getValue().getName() + entry.getValue().getAge());
    }
    Set<String> set = map.keySet();
    for (String s : set) {
        System.out.println(s + map.get(s).getName() + map.get(s).getAge());
    }
}

这个项目中,想要保留键的唯一性的案例,就需要在键的类(Student)中重写equals方法和hashCode方法

ArrayList集合存储HashMap集合元素并遍历

①创建ArrayList集合

②创建HashMap集合,并添加键值对元素

③把HashMap作为元素添加到ArrayList集合

④遍历ArrayList集合

public static void main(String[] args) {
    ArrayList<HashMap<String, String>> arrayList = new ArrayList<HashMap<String, String>>();
    HashMap<String, String> hashMap1 = new HashMap<String, String>();
    hashMap1.put("周瑜", "小乔");
    HashMap<String, String> hashMap2 = new HashMap<String, String>();
    hashMap2.put("孙策", "大乔");
    arrayList.add(hashMap1);
    arrayList.add(hashMap2);
    System.out.println(arrayList);
    for (HashMap<String, String> hashMap : arrayList) {
        Set<String> set = hashMap.keySet();
        for (String key : set) {
            System.out.println(key + "," + hashMap.get(key));
        }
    }
}
HashMap集合存储ArrayList元素并遍历

①创建HashMap集合

②创建ArrayList集合,添加元素

③把ArrayList元素作为元素添加进HashMap中

public static void main(String[] args) {
    HashMap<String, ArrayList<String>> hashMap = new HashMap<String, ArrayList<String>>();
    ArrayList<String> arrayList1 = new ArrayList<String>();
    arrayList1.add("诸葛亮");
    arrayList1.add("赵云");
    ArrayList<String> arrayList2 = new ArrayList<String>();
    arrayList2.add("贾宝玉");
    arrayList2.add("林黛玉");
    hashMap.put("三国演义", arrayList1);
    hashMap.put("红楼梦", arrayList2);
    Set<String> set = hashMap.keySet();
    for (String key : set) {
        System.out.println("《" + key + "》");
        ArrayList<String> arrayList = hashMap.get(key);
        for (String s : arrayList) {
            System.out.println(s);
        }
        System.out.println("《" + key + "》" + ": " + arrayList);
    }
}
统计字符串中每个字符出现的次数

要求编写一个程序,接收输入的字符串,统计字符串出现的次数并按照要求的格式输出

请输入字符:
cccbbbaaaddd
a(3)b(3)c(3)d(3)

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入字符:");
    String line = scanner.nextLine();
    // 创建HashMap存储结果
    // 如果想要排序的结果,使用treeMap即可
    // HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>();
    TreeMap<Character, Integer> hashMap = new TreeMap<Character, Integer>();
    // 遍历每一个字符,得到各字符对应的数字
    for (int i = 0; i < line.length() ; i++) {
        char key = line.charAt(i);
        // 去 hashMap 中寻找看看是否存在,不存在就添加,存在就+1
        Integer value = hashMap.get(key);

        if (value == null) {
            hashMap.put(key, 1);
        } else {
            value++; // 此处进行了拆箱操作,需要再进行装箱操作,才能传进去
            hashMap.put(key, value);
        }
    }

    // 遍历HashMap集合,按要求输出结果
    StringBuilder stringBuilder = new StringBuilder();

    Set<Character> characters = hashMap.keySet();
    for (Character key : characters) {
        stringBuilder.append(key).append("(").append(hashMap.get(key)).append(")");
    }
    System.out.println(stringBuilder);
}

Collections

image-20210429191917166

public static void main(String[] args) {
    List<Integer> arrayList = new ArrayList<Integer>();
    arrayList.add(40);
    arrayList.add(20);
    arrayList.add(10);
    arrayList.add(50);
    arrayList.add(30);
    // 将list中的元素反转顺序输出
    Collections.reverse(arrayList);
    System.out.println(arrayList);
    // 将list中的元素排序输出
    Collections.sort(arrayList);
    System.out.println(arrayList);
    // 将list中的元素按照随机顺序排序
    Collections.shuffle(arrayList);
    System.out.println(arrayList);
}

输出结果中,第三行的结果每一次都不一样

[30, 50, 10, 20, 40]
[10, 20, 30, 40, 50]
[50, 30, 40, 20, 10]

ArrayList集合存储学生对象并排序

使用ArrayList存储学生对象并排序,利用Collections对学生进行排序,并遍历ArrayList

public static void main(String[] args) {
    ArrayList<Student> students = new ArrayList<Student>();
    Student student1 = new Student("zhangsan", 20);
    Student student2 = new Student("lisi", 19);
    Student student3 = new Student("wangwu", 18);
    Student student4 = new Student("wangw", 18);

    students.add(student1);
    students.add(student2);
    students.add(student3);
    students.add(student4);

    // 第一种方法,在Student内部实现Comparable
    // Collections.sort(students);
    // 第二种方法,使用匿名内部类
    Collections.sort(students, new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
            int num = s1.getAge() - s2.getAge();
            int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
            return num2;
        }
    });

    System.out.println(students);

    for (Student student : students) {
        System.out.println(student);
    }
}
案例:模拟斗地主中的洗牌、发牌、看牌

①创建一个牌盒,也就是创建一个ArrayList集合对象

②往牌盒里面装牌,添加元素

③洗牌,把牌的顺序打乱,用shuffle方法实现

④发牌,遍历集合,给三个玩家发牌

⑤看牌,三个玩家分别遍历自己的牌

public class PokerSimulation {
    /*
    * 模拟斗地主中的洗牌、发牌和看牌
    * ①创建一个牌盒,也就是创建一个ArrayList集合对象
    * ②往牌盒里面装牌,添加元素
    * ③洗牌,把牌的顺序打乱,用shuffle方法实现
    * ④发牌,遍历集合,给三个玩家发牌
    * ⑤看牌,三个玩家分别遍历自己的牌
    */
    public static void main(String[] args) {
        // ①创建一个牌盒,也就是创建一个ArrayList集合对象
        ArrayList<String> array = new ArrayList<String>();
        // ②往牌盒里面装牌,添加元素
        /*
        * Joker1、Joker2
        * ♦2、♦3、、、♦K、♦A
        * ♣2、♣3、、、
        * ♠2、♠3、、、
        * ♥2、♥3、、、
        * */
        // 定义花色数组
        String[] colors = {"♠", "♥", "♣", "♦"};
        // 定义点数数组
        String[] numbers = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
        // 添加进ArrayList
        for (String color : colors) {
            for (String number : numbers) {
                array.add(color + number);
            }
        }
        array.add("JokerSmall");
        array.add("JokerBig");
        // ③洗牌,把牌的顺序打乱,用shuffle方法实现
        Collections.shuffle(array);
        // ④发牌,遍历集合,给三个玩家发牌
        ArrayList<String> user1 = new ArrayList<String>();
        ArrayList<String> user2 = new ArrayList<String>();
        ArrayList<String> user3 = new ArrayList<String>();
        ArrayList<String> landlord = new ArrayList<String>();
        for (int i = 0; i < array.size(); i++) {
            String poke = array.get(i);
            if (i >= array.size() - 3) {
                landlord.add(poke);
            } else if (i % 3 == 0) {
                user1.add(poke);
            } else if (i % 3 == 1) {
                user2.add(poke);
            } else if (i % 3 == 2) {
                user3.add(poke);
            }
        }
        // ⑤看牌,三个玩家分别遍历自己的牌
        lookPoke("张三", user1);
        lookPoke("李四", user2);
        lookPoke("王五", user3);
        lookPoke("底牌", landlord);

        System.out.println(array);
    }

    private static void lookPoke (String name, ArrayList<String> arrayList) {
        System.out.print(name + "的牌是: ");
        for (String poke : arrayList) {
            System.out.print(poke + " ");
        }
        System.out.println();
    }

}

整体运行结果如下所示:

张三的牌是: ♥7 ♥Q ♠3 ♦6 ♦J ♥3 ♣5 ♥9 ♥4 ♠J ♠10 ♠4 ♥8 ♦K ♦A ♣9 ♠K
李四的牌是: ♣2 ♥K ♦9 ♦3 ♣J ♥10 ♣4 ♣8 ♠6 ♠9 ♠A ♠7 ♠8 ♦7 ♥5 ♣7 ♣K
王五的牌是: ♠2 ♥A ♦10 ♦Q ♦4 ♣3 ♠Q JokerSmall ♠5 ♥2 ♣Q ♦2 ♣6 ♦5 ♦8 ♣10 ♣A
底牌的牌是: ♥J ♥6 JokerBig

案例:将斗地主中的牌进行排序

image-20210429203158066

①创建HashMao集合,键是编号,值是牌

②创建ArrayLIst存储编号

③将三个玩家的牌的编号存进TreeSet中

④将TreeSet中的编号取出来,从HashMap中获得对应的牌

⑤洗牌,将编号打乱,用Collections中的shuffle方法打乱

⑥发牌,发的也是编号,将编号用TreeSet存储,会直接输出有序序列

⑦看牌,定义看牌方法,根据编号从HashMap中获取到牌

⑧调用看牌方法

实现:

public class PokeDemo {
    public static void main(String[] args) {
        // ①创建HashMao集合,键是编号,值是牌
        HashMap<Integer, String> hashMap = new HashMap<Integer, String>();
        // ②创建ArrayList存储编号
        ArrayList<Integer> array = new ArrayList<Integer>();
        // 定义花色数组
        String[] colors = {"♦", "♣", "♥", "♠"};
        // 定义点数数组
        String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
        // 定义编号,从0开始往HashMap中存储数据
        int index = 0;
        for (String number : numbers) {
            for (String color : colors) {
                hashMap.put(index, color + number);
                array.add(index);
                index++;
            }
        }
        hashMap.put(index, "JokerSmall");
        array.add(index);
        index++;
        hashMap.put(index, "JokerBig");
        array.add(index);
        // ③洗牌,将编号打乱,用Collections中的shuffle方法打乱
        Collections.shuffle(array);
        // ④发牌,发的也是编号,将编号用TreeSet存储,会直接输出有序序列
        TreeSet<Integer> user1 = new TreeSet<Integer>();
        TreeSet<Integer> user2 = new TreeSet<Integer>();
        TreeSet<Integer> user3 = new TreeSet<Integer>();
        TreeSet<Integer> landlord = new TreeSet<Integer>();

        for (int i = 0; i < array.size(); i++) {
            if (i >= array.size() - 3) {
                landlord.add(array.get(i));
            } else if (i % 3 == 0) {
                user1.add(array.get(i));
            } else if (i % 3 == 1) {
                user2.add(array.get(i));
            } else if (i % 3 == 2) {
                user3.add(array.get(i));
            }
        }

        // ⑥调用看牌方法
        lookPoke("张三", user1, hashMap);
        lookPoke("李四", user2, hashMap);
        lookPoke("王五", user3, hashMap);
        lookPoke("底牌", landlord, hashMap);
    }

    // ⑤看牌,定义看牌方法,根据编号从HashMap中获取到牌
    private static void lookPoke(String name, TreeSet<Integer> treeSet, HashMap<Integer, String> hashMap) {
        System.out.print(name + "的牌是: ");
        for (Integer key : treeSet) {
            String value = hashMap.get(key);
            System.out.print(value + " ");
        }
        System.out.println();
    }
}

六、IO流

File

是文件和目录的抽象表示

  • 文件和目录是可以通过File封装成对象的
  • 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。它可以是存在的,也可以是不存在的。将来是要通过具体的操作把这个路径的内容转换为具体存在的

image-20210429213119090

public static void main(String[] args) {
    File file1 = new File("D:\\Java\\java.txt");
    System.out.println(file1);
    File file2 = new File("D:\\Java", "java.txt");
    System.out.println(file2);
    File file3 = new File("D:\\Java");
    File file4 = new File(file3, "java.txt");
    System.out.println(file4);
}

输出内容:

D:\Java\java.txt
D:\Java\java.txt
D:\Java\java.txt

磁盘中并没有添加新的文件,路径下没有新建的java.txt文件

File类创建功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4eAlKFi-1639662744847)(https://i.loli.net/2021/04/29/mzpvDVAc59SNuB6.png)]

public static void main(String[] args) throws IOException {
    File file1 = new File("D:\\Java\\java.txt");
    // boolean createNewFile 创建新文件,成功返回true,否则false
    System.out.println(file1.createNewFile());

    File file2 = new File("D:\\Java\\Test");
    // boolean mkdir 创建对应的目录,成功返回true,否则false
    // 创建由此命名的抽象目录
    System.out.println(file2.mkdir());

    File file3 = new File("D:Java\\JTest\\HTML");
    // boolean mkdirs 创建对应的抽象目录,成功返回true,否则false
    // 创建由此命名的抽象目录,包括必需但不存在的父目录
    System.out.println(file3.mkdirs());
}
File类判断和获取功能

image-20210429215744326

public static void main(String[] args) throws IOException {
    File file = new File("D:\\Java\\java.txt");
    file.createNewFile();
    System.out.println(file.isDirectory());
    System.out.println(file.isFile());
    System.out.println(file.exists());

    System.out.println(file.getAbsolutePath()); // 返回文件的绝对路径
    System.out.println(file.getPath()); // 将此抽象路径名转换成字符串
    System.out.println(file.getName()); // 返回文件的名称
    System.out.println("-------------");

    File file1 = new File("D:\\Java");
    String[] list = file1.list(); // 返回的是抽象目录下的文件以及文件目录对应的名称字符串数组
    for (String str : list) {
        System.out.println(str);
    }
    System.out.println("-------------");
    File[] files = file1.listFiles(); // 返回此抽象目录中的文件以及文件目录的File对象
    for (File f : files) {
        System.out.println(f);
    }
}

输出结果:

false
true
true
D:\Java\java.txt
D:\Java\java.txt

java.txt

apache-tomcat-8.5.65
apache-tomcat-8.5.65-windows-x64.zip
D:\Java\apache-tomcat-8.5.65
D:\Java\apache-tomcat-8.5.65-windows-x64.zip

Process finished with exit code 0

File类删除功能

image-20210430153149746

public static void main(String[] args) throws IOException {
    File file = new File("D:\\Java\\java.txt");
    System.out.println(file.createNewFile());
    boolean delete = file.delete();   // boolean delete()返回的是布尔值,删除操作成功返回true
    System.out.println(delete);
    File file1 = new File("D:\\Java\\javatest");
    System.out.println(file1.mkdir());
    System.out.println(file1.delete());
}

递归

递归,指的是程序方法调用方法本身

解决的问题:

把一个复杂的问题层层转化为一个个与原问题相似的较小的问题

递归策略只需要少量的程序就可以描述出解题过程中所需要的多次重复计算

/*打印斐波那契数列*/
public static void main(String[] args) {
    System.out.println(printNumber(20));
}

private static int printNumber(int n) {
    if (n == 1 || n == 2) {
        return 1;
    } else {
        return printNumber(n - 1) + printNumber(n - 2);
    }
}
案例:递归求阶乘
public static void main(String[] args) {
    System.out.println(factorial(5));
}
private static int factorial(int n) {
    if (n == 1) {
        return 1;
    } else {
        return factorial(n - 1) * n;
    }
}

运行结果:正常打印120

运行时的内存图:进栈过程

image-20210430154848124

运行时的内存图:出栈过程

image-20210430154933302

案例:遍历目录

给定一个指定的目录路径,遍历该目录下的所有内容,并将文件的绝对路径名打印出来

public static void main(String[] args) {
    // ①根据给定的路径创建一个file对象
    File srcFile = new File("D:\\Java");
    // ⑥调用方法
    getFilePath(srcFile);
}
// ②定义一个方法,用于获取给定目录下的所有内容
private static void getFilePath(File srcFile) {
    // ③获取给定的File目录下的所有文件或者目录的File[]数组
    File[] files = srcFile.listFiles();
    // ④遍历该File数组,得到每一个对象
    if (files != null) {
        for (File file : files) {
            // ⑤判断该File对象是否是目录
            if (file.isDirectory()) {
                getFilePath(file); // 是目录,递归调用
            } else if (file.isFile()) {
                System.out.println(file.getAbsolutePath()); // 是文件,直接打印输出
            }
        }
    }
}

输出的是所有文件的绝对路径名以及文件名称、后缀名

字节流

  • IO:input/output,输入输出
  • 流:是一种抽象概念,是数据传输的总称,也就是说数据在设备之间的传输称为流,流的本质是数据传输
  • IO流就是用来处理设备间的数据传输问题
    • 常见的应用:文件复制、文件上传、文件下载
分类
  • 按照数据的流向
    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型
    • 字节流
      • 字节输入流、字节输出流
    • 字符流
      • 字符输入流、字符输出流

IO流的分类默认是按照数据类型来分类的。默认情况下,如果用记事本打开文件之后能读懂的内容,就使用字符流,否则字节流。

如果在不知道什么文件的情况下,使用字节流。

字节流写数据
  • InputStream:这个抽象类是表示字节输入流的所有类的超类

  • OutputStream:这个抽象类是表示字节输出流的所有类的超类

  • 子类名特点:子类名称都是以其父类名称作为子类名的后缀

FileOutputStream:文件输出流用于将数据写入File

  • FileOutputStream(String name):创建文件输出流以指定的名称写入文件
  • FileOutputStream(File file):创建文件输出流以写入由指定的File对象表示的文件
  • FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件,第二个参数为true,则从文件末尾写入数据而不是文件开头

使用字节输出流写数据的步骤:

  • 创建字节输出流对象:调用系统功能创建了文件,创建字节输出流对象,让字节输出流对象指向文件
  • 调用字节输出流的写数据方法
  • 释放资源:关闭字节输出流对象以及所有和字节输出流相关的系统资源
字节流写数据的三种方式

image-20210430180513479

public static void main(String[] args) throws IOException {
    FileOutputStream fos = new FileOutputStream("D:\\Java\\java.txt");
    /*File file = new File("D:\\Java\\java.txt");
    FileOutputStream fos2 = new FileOutputStream(file);*/
    for (int i = 97; i <= 101; i++) {
        // 将指定的字节写入此文件输出流
        fos.write(i);
    }
    // 将b.length字节数组写入此文件输出流
    // byte[] bytes = {97, 98, 99, 100, 101};
    byte[] bytes = "abcde".getBytes(StandardCharsets.UTF_8);
    fos.write(bytes);
    // 将bytes数组从指定的位置开始,以指定的偏移量开始迁移,将这些字符写入该文件输出流
    // write(byte[] b, int off, int len)
    // fos.write(bytes, 0, bytes.length);
    fos.write(bytes, 1, 3);
    // 释放资源
    fos.close();
}

这里写入方法中,写入的内容,都需要转换为ASCII码对应的形式,或者直接调用getBytes方法转换。

①字节流写数据如何实现换行:

根据不同的系统,在写完数据的时候追加不同的换行符

WIndows:\r\n LInux:\n Mac:\r

// 写数据
for (int i = 0; i < 10; i++) {
    fos.write("hello".getBytes());
    /*Windows:\r\n,Linux:\n,Mac:\r*/
    fos.write("\r\n".getBytes());
}

②字节流写数据如何追加数据:

字节流在写数据的时候,会将同名的文件内容清空

我们使用下面这种构造方法创建文件输出流对象

FileOutputStream(String name, boolean append):创建文件输出流以指定的名称写入文件,第二个参数为true,则从文件末尾追加数据而不是开头

public static void main(String[] args) throws IOException {
    FileOutputStream fos = new FileOutputStream("D:\\Java\\java.txt", true);
    // 写数据
    for (int i = 0; i < 10; i++) {
        fos.write("hello".getBytes());
        /*Windows:\r\n,Linux:\n,Mac:\r*/
        fos.write("\r\n".getBytes());
    }
    // 释放资源
    fos.close();
}
字节流写数据的异常处理

之前在写字节输出流的时候,有异常情况都是直接throws抛出,这个时候我们自己编写代码,捕获一场并进行处理操作

public static void main(String[] args) {
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream("D:\\Java\\java.txt", true);
        fos.write("World".getBytes());
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
字节流读数据(一次读一个数据)

FileInputStream:从文件系统中的文件获取输入字节

需求:把之前创建的java.txt文件中的数据读取出来在控制台输出

FileInputStream(String name):通过打开与实际文件的连接创建一个FileInputStream对象,该文件由文件系统中的文件路径名+文件名命名

public static void main(String[] args) throws IOException {
    // 创建字节流输入流对象
    FileInputStream fis = new FileInputStream("D:\\Java\\java.txt");
    // 调用字节输入流对象中的读取方法
    // int read() : 从字节输入流中读取一个字节的数据
    /*int read = fis.read();
    System.out.println(read);
    System.out.println((char) read);
    // 第二次读取数据(如果文件到达末尾,则返回-1)
    read = fis.read();*/
    /*int read = fis.read();
    while (read != -1) {
        System.out.print((char) read);
        read = fis.read();
    }*/
    int read;
    while ((read = fis.read()) != -1) {
        System.out.print((char) read);
    }
    System.out.println(read);
    System.out.println((char) read);
    // 关闭资源
    fis.close();
}

具体操作如上所示,最终打印的结果跟文件中的内容一致

案例:复制文本文件

需求:将文件D:\Java\java.txt文件复制到模块目录下

分析:复制文本文件,其实就是将文本文件中的内容读取出来,写入到目的路径下的相同类型的文件中

public static void main(String[] args) throws IOException {
    // 根据数据源创建字节输入流对象,读取操作
    FileInputStream fis = new FileInputStream("D:\\Java\\java.txt");
    // 根据目的地创建字节输出流对象,写入操作
    FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt");
    // 读取数据,一次性读入一个字节,一次性写入一个字节
    int by;
    while ((by = fis.read()) != -1) {
        fos.write(by);
    }
    // 释放资源
    fos.close();
    fis.close();
}

运行结果如下:在工作空间下的模块目录中新增了java.txt文件

image-20210430203019769

字节流读数据(一次读一个字节数组数据)

把文件中的内容读取出来在控制台输出

public static void main(String[] args) throws IOException {
    // 创建字节输入流对象
    FileInputStream fis =new FileInputStream("D:\\Java\\java.txt");

    // 调用字节输入流的读数据方法
    byte[] bytes = new byte[1024];
    /*int len = fis.read(bytes);
    System.out.println(len);
    System.out.println(new String(bytes, 0, len));*/
    int len;
    while ((len = fis.read(bytes)) != -1) {
        System.out.print(new String(bytes, 0, len));
    }

    // 释放资源
    fis.close();
}

最终读取出来的就是文件中的内容

案例:复制图片
public static void main(String[] args) throws IOException {
    // 根据数据源对象创建字节输入流对象
    FileInputStream fis = new FileInputStream("D:\\1.png");
    // 根据目的地创建字节输出流
    FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\1.png");

    // 复制操作
    byte[] bytes = new byte[1024];
    int len;
    while ((len = fis.read()) != -1) {
        fos.write(bytes, 0, len);
    }

    // 关闭资源
    fos.close();
    fis.close();
}

这样编写运行之后,图片就会从输入流的路径复制到输出流的路径下

字节缓冲流

image-20210430210639846

使用过程

public static void main(String[] args) throws IOException {
    // 创建缓冲字节输出流
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\bos.txt"));
    // 写数据
    bos.write("hello\r\n".getBytes());
    bos.write("world\r\n".getBytes());
    // 释放资源
    bos.close();
    // 创建缓冲字节输入流
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\bos.txt"));
    // 读取文件数据
    byte[] bytes = new byte[1024];
    int len;
    while ((len = bis.read(bytes)) != -1) {
        System.out.print(new String(bytes, 0, len));
    }
    // 释放资源
    bis.close();
}
案例:复制视频

需求:复制视频文件,将一个路径下的视频文件复制到另外的路径下

public class CopyAviDemo {
    public static void main(String[] args) throws IOException {
        // 创建起始时间
        long startTime = System.currentTimeMillis();

        method1();

        // 结束时间
        long endTime = System.currentTimeMillis();
        // 一共耗时多少秒
        long totalTime = endTime - startTime;
        System.out.println("一共耗时:" + totalTime + "毫秒");
    }

    // 字节缓冲流一次读取一个字节数组
    private static void method4() throws IOException {
        // 字节缓冲流复制视频文件
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\demo.avi"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi"))// 复制操作
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        // 关闭资源
        bos.close();
        bis.close();
    }

    // 字节缓冲流一次读取一个字节
    private static void method3() throws IOException {
        // 字节缓冲流复制视频文件
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Java\\demo.avi"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi"))// 复制操作
        int by;
        while ((by = bis.read()) != -1) {
            bos.write(by);
        }

        // 关闭资源
        bos.close();
        bis.close();
    }

    // 基本字节流一次读取一个字节数组
    private static void method2() throws IOException {
        // 字节流复制视频
        FileInputStream fis = new FileInputStream("D:\\Java\\demo.avi");
        FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi");

        // 复制操作
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }

        // 关闭资源
        fos.close();
        fis.close();
    }


    // 基本字节流一次读取一个字节
    private static void method1() throws IOException {
        // 字节流复制视频
        FileInputStream fis = new FileInputStream("D:\\Java\\demo.avi");
        FileOutputStream fos = new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\demo.avi");

        // 复制视频操作
        int by;
        while ((by = fis.read()) != -1) {
            fos.write(by);
        }

        // 释放资源
        fos.close();
        fis.close();

    }
}

对比这四种方式,根据运行时间可以得到,字节缓冲流的速度大于字节流,读写字符数组的方式速度大于读写单个字节的速度

字符流

image-20210501145954622

/*
* 单个汉字在UTF-8编码下占3个字节,在GBK编码下占2个字节
* */
public static void main(String[] args) throws UnsupportedEncodingException {
    // String s = "abc"; // [97, 98, 99]
    String s = "中国"; // UTF-8:[-28, -72, -83, -27, -101, -67]
    // GBK:[-42, -48, -71, -6]
    byte[] bytes = s.getBytes("GBK");
    System.out.println(Arrays.toString(bytes));
}
编码表

image-20210501151654340

image-20210501151717326

image-20210501151819302

image-20210501152003084

image-20210501152119765

字符串中编码解码问题

image-20210501152228740

public static void main(String[] args) throws UnsupportedEncodingException {
    String s = "中国";
    byte[] bytes = s.getBytes(); // 默认使用UTF-8进行编码
    String ss = new String(bytes, "UTF-8");
    System.out.println(Arrays.toString(bytes));  // [-28, -72, -83, -27, -101, -67]
    System.out.println(ss);
    byte[] bytes1 = s.getBytes("GBK");
    String ss1 = new String(bytes1, "GBK");
    System.out.println(Arrays.toString(bytes1)); // [-42, -48, -71, -6]
    System.out.println(ss1);
}

使用何种方式编码,就要使用何种方式解码,否则将会出现乱码问题。

字符流中的编码解码问题

字符流的基类

  • Reader:字符输入流的抽象类
  • Writer:字符输出流的抽象类

字符流中和编码解码相关的两个类

  • InputStreamReader
  • OutputStreamWriter
public static void main(String[] args) throws IOException {
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt"), "GBK");
    osw.write("中国");
    osw.close();
    InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\java.txt"), "GBK");
    int len;
    while ((len = isr.read()) != -1) {
        System.out.print((char) len);
    }
    isr.close();
}
字符流写数据的五种方式

image-20210501182733661

image-20210504133020515

public static void main(String[] args) throws IOException {
    // 创建osw对象
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\osw.txt"));
    // 写数据,此时数据还在缓冲区
    osw.write(97);
    // 调用刷新流,将数据从缓冲区转移到目的文件中
    // osw.flush();

    // 写入一个数组
    char[] bytes = {'h', 'e', 'l', 'l', 'o'};
    osw.write(bytes/*, 0, bytes.length*/);

    // 写入一个字符串
    osw.write("world");

    // 写入一个字符串的一部分
    osw.write("java", 0, 1);

    // 关闭资源,但是在关闭之前会自动调用一次刷新
    osw.close();
}
字符流读数据的两种方式

image-20210504133127153

public static void main(String[] args) throws IOException {
    // 创建字符流读取对象
    InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\osw.txt"));
    // 读取数据,一次读取一个字符
    int ch;
    while ((ch = isr.read()) != -1) {
        System.out.print((char) ch);
    }
    // 一次性读入一个字符串
    char[] chs = new char[1024];
    int len;
    while ((len = isr.read(chs)) != -1) {
        System.out.print(new String(chs, 0, len));
    }
    // 释放资源
    isr.close();
}
案例:复制java文件
public static void main(String[] args) throws IOException {
    // 根据源文件创建字符输入流
    InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
    // 根据目的地创建字符输出流
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Java\\IdeaProjects\\JavaBasic\\ConversionStringDemo.java"));

    // 复制文件操作
    /*int ch;
    while ((ch = isr.read()) != -1) {
        osw.write(ch);
    }*/

    char[] chs = new char[1024];
    int len;
    while ((len = isr.read(chs)) != -1) {
        osw.write(chs);
    }

    // 释放资源
    osw.close();
    isr.close();
}
案例:复制Java文件(改进版)

image-20210504141549380

public static void main(String[] args) throws IOException {
    // 根据源文件创建输入流对象
    FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java");
    // 根据目的地创建输出流对象
    FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\ConversionStringDemo.java");

    // 读写数据,复制文件
    int ch;
    while ((ch = fr.read()) != -1) {
        fw.write(ch);
    }

    char[] chars = new char[1024];
    int len;
    while ((len = fr.read(chars)) != -1) {
        fw.write(chars, 0, len);
    }

    // 释放资源
    fw.close();
    fr.close();
}

字符缓冲流

image-20210507125006043

public static void main(String[] args) throws IOException {
    /*BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bw.txt"));
    bw.write("hello\r\n");
    bw.write("world\r\n");*/
    
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bw.txt"));

    // 一次读取一个字符
    /*int ch;
    while ((ch = br.read()) != -1) {
        System.out.print((char) ch);
    }*/

    // 一次读取一个字符数组
    char[] chs = new char[1024];
    int len;
    while ((len = br.read(chs)) != -1) {
        System.out.print(new String(chs, 0, len));
    }

    // 关闭资源
    br.close();
    // bw.close();
}
案例:复制Java文件

image-20210507130025862

public static void main(String[] args) throws IOException {
    // 创建字符缓冲流读取对象
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
    // 创建字符缓冲流写入对象
    BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\Copy.java"));
    // 一次读入一个字符
    /*int ch;
    while ((ch = br.read()) != -1) {
        bw.write(ch);
    }*/
    // 一次读取一个字符数组
    char[] chs = new char[1024];
    int len;
    while ((len = br.read(chs)) != -1) {
        bw.write(chs, 0, len);
    }

    // 释放资源
    bw.close();
    br.close();
}
字符缓冲流特有功能

image-20210507130849566

public static void main(String[] args) throws IOException {
    /*BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bwDemo.txt"));

    for (int i = 1; i <= 10; i++) {
        bw.write("hello" + i);
        // bw.write("\r\n");
        bw.newLine();
        bw.flush();
    }

    // 释放资源
    bw.close();*/

    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\bwDemo.txt"));

    /*String line = br.readLine();
    System.out.println(line);
    line = br.readLine();
    System.out.println(line);
    line = br.readLine();
    System.out.println(line);
    line = br.readLine();
    System.out.println(line);
    // 当最终没有数据的时候,会输出null*/

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

    // 释放资源
    br.close();
}
案例:复制Java文件(字符缓冲流特有功能实现)

image-20210507132437294

public static void main(String[] args) throws IOException {
    // 根据数据源创建字符输入流对象
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_06\\ConversionStringDemo.java"));
    // 根据目的源创建字符输入流对象
    BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_07\\Copy.java"));
    // 读写数据,复制文件
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine();
        bw.flush();
    }
    // 释放资源
    bw.close();
    br.close();
}

IO流小结

image-20210507133726089

image-20210507133759490

案例:集合到文件

需求:把ArrayList集合中的数据写入到文本文件中;要求:每一个字符串元素作为文件中的一行数据

public static void main(String[] args) throws IOException {
    // 创建ArrayList对象
    ArrayList<String> arrayList = new ArrayList<String>();
    arrayList.add("Hello");
    arrayList.add("World");
    arrayList.add("Java");
    // 创建字符缓冲输出流对象
    BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\ArrayList.txt"));
    for (String line : arrayList) {
        bw.write(line);
        bw.newLine();
        bw.flush();
    }
    // 释放资源
    bw.close();
}
案例:文件到集合

需求:与上述案例相反,将文件中的内容输出到集合中。

public static void main(String[] args) throws IOException {
    // 创建字符缓冲输入流对象
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\ArrayList.txt"));
    // 创建 ArrayList 集合对象
    ArrayList<String> arrayList = new ArrayList<String>();
    // 遍历文件,得到文本数据
    String line;
    while ((line = br.readLine()) != null) {
        arrayList.add(line);
    }
    // 释放资源
    br.close();
    // 遍历集合
    for (String s : arrayList) {
        System.out.println(s);
    }
}
案例:点名器

需求:有一个文件中存储着班级同学的姓名,每一个姓名占一行,要求通过程序实现随机点名器

实现的思路:将文件中的姓名输入到集合中,之后在集合的范围中产生随机数作为索引,访问到相对应的同学姓名

public static void main(String[] args) throws IOException {
    // 创建字符缓冲输入流对象
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_08\\names.txt"));
    // 创建 ArrayList 集合对象
    ArrayList<String> arrayList = new ArrayList<String>();
    // 读写文件数据,写入集合
    String line;
    while ((line = br.readLine()) != null) {
        arrayList.add(line);
    }
    // 释放资源
    br.close();
    // 使用随机数对象产生随机数,范围为[0,arraylist.size())
    Random random =  new Random();
    int index = random.nextInt(arrayList.size());
    // 利用索引找到集合中对应的元素
    String name = arrayList.get(index);
    // 输出随机抽取到的姓名
    System.out.println(name);
}
案例:集合到文件(改进版)

需求:把ArrayList中的学生数据写入到文本文件,要求一个学生信息在同一行的位置

格式:学号,姓名,年龄,居住地

public static void main(String[] args) throws IOException {
    // 创建集合对象
    ArrayList<Student> array = new ArrayList<Student>();
    // 创建学生对象
    Student s1 = new Student("1840800", "张三", 19, "北京");
    Student s2 = new Student("1840801", "张三", 20, "天津");
    Student s3 = new Student("1840802", "张三", 21, "上海");
    Student s4 = new Student("1840803", "张三", 22, "重庆");
    // 把学生对象添加进集合
    array.add(s1);
    array.add(s2);
    array.add(s3);
    array.add(s4);
    // 创建字符缓冲输出流对象
    BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_09\\Students.txt"));
    // 遍历集合,得到每一个学生对象
    for (Student student : array) {
        // 把学生对象数据拼接成指定格式的字符串
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(student.getStuId()).append(",").append(student.getName()).append(",").append(student.getAge()).append(",").append(student.getAddress());
        // 调用字符缓冲输出流对象写数据
        bw.write(stringBuilder.toString());
        bw.newLine();
        bw.flush();
    }
    // 释放资源
    bw.close();
}
案例:文本到集合(改进版)

需求:将文本文件中的数据读取出来写入到集合中并实现遍历

要求每一行数据是一个对象的数据

public static void main(String[] args) throws IOException {
    // 首先创建字符缓冲输入流
    BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_09\\Students.txt"));
    // 根据需求创建 ArrayList 集合对象
    ArrayList<Student> array = new ArrayList<Student>();
    // 读取文本数据,读取出对象内容
    String line;
    while ((line = br.readLine()) != null) {
        // 用 Split 方法分割读取到的字符串
        String[] split = line.split(",");
        // 格式:学号,姓名,年龄,住址
        Student student = new Student(split[0], split[1], Integer.parseInt(split[2]), split[3]);
        array.add(student);
    }
    // 释放资源
    br.close();
    // 遍历集合对象
    for (Student student : array) {
        System.out.println(student);
    }
}
案例:集合到文件(数据排序改进版)

需求:键盘录入学生信息(姓名,语文成绩,数学成绩,英语成绩),要求将学生成绩按照总分降序排进文本文件

格式:姓名,语文成绩,数学成绩,英语成绩

思路:创建TreeSet对象实现

步骤一:创建Student类

public class Student {
    private String name;
    private int Chinese;
    private int Mathematics;
    private int English;

    public int getSum() {
        return this.Chinese + this.Mathematics + this.English;
    }

    public Student(String name, int chinese, int mathematics, int english) {
        this.name = name;
        Chinese = chinese;
        Mathematics = mathematics;
        English = english;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getChinese() {
        return Chinese;
    }

    public void setChinese(int chinese) {
        Chinese = chinese;
    }

    public int getMathematics() {
        return Mathematics;
    }

    public void setMathematics(int mathematics) {
        Mathematics = mathematics;
    }

    public int getEnglish() {
        return English;
    }

    public void setEnglish(int english) {
        English = english;
    }
}

步骤二:创建主类

public class TreeSetToFile {
    public static void main(String[] args) throws IOException {
        // 创建 TreeSet 集合对象
        TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                // 主要条件:总分是否相同
                int num = s2.getSum() - s1.getSum();
                // 次要条件:科目分数是否相同
                int num2 = num == 0 ? s2.getChinese() - s1.getChinese() : num;
                int num3 = num2 == 0 ? s2.getMathematics() - s1.getMathematics() : num2;
                // 次要条件:姓名是否相同
                int num4 = num3 == 0 ? s2.getName().compareTo(s1.getName()) : num3;
                return num4;
            }
        });
        // 从键盘录入学生数据
        for (int i = 0; i < 5; i++) {
            Scanner sc = new Scanner(System.in);
            System.out.println("请录入第" + (i + 1) + "个学生信息:");
            System.out.println("请输入姓名:");
            String name = sc.nextLine();
            System.out.println("语文成绩:");
            int Chinese = sc.nextInt();
            System.out.println("数学成绩:");
            int Mathematics = sc.nextInt();
            System.out.println("英语成绩:");
            int English = sc.nextInt();
            // 创建学生对象
            Student student = new Student(name, Chinese, Mathematics, English);
            // 把学生对象添加进集合
            ts.add(student);
        }

        // 创建字符缓冲输出流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_10\\ts.txt"));

        // 遍历学生对象,把学生对象的数据拼接成指定格式的字符串内容
        for (Student student : ts) {
            // 格式:姓名,语文成绩,数学成绩,英语成绩
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(student.getName()).append(",").append(student.getChinese()).append(",").append(student.getMathematics()).append(",").append(student.getEnglish()).append(", 总分:").append(student.getSum());
            // 调用字符缓冲输出流对象写数据
            bw.write(stringBuilder.toString());
            bw.newLine();
            bw.flush();
        }

        // 释放资源
        bw.close();
    }
}
案例:复制单级文件夹

image-20210508162805260

复制单级文件夹,但是由于文件夹中的文件不是单一格式,所以我们采用字节流复制文件

public class CopySingleFileFolder {
    public static void main(String[] args) throws IOException {
        // 创建源文件夹对象
        File srcFile = new File("D:\\temp");
        // 获取文件夹名称
        String srcName = srcFile.getName();
        // 创建目的文件夹对象
        File destFolder = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_11", srcName);
        // 判断文件夹对象是否存在
        if (! destFolder.exists()) {
            destFolder.mkdir();
        }

        // 获取数据源目录下的所有文件对象
        File[] listFiles = srcFile.listFiles();

        // 遍历 listFiles 数组,将文件写入目的文件夹中
        for (File file : listFiles) {
            // 获取源文件的名称
            String srcFileName = file.getName();
            // 创建目的 File 对象
            File destFile = new File(destFolder, srcFileName);
            // 复制文件
            copyFile (srcFile, destFile);
        }
    }

    private static void copyFile(File srcFile, File destFile) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));

        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        bos.close();
        bis.close();
    }
}
案例:复制多级文件夹

需求:复制多级文件夹,该文件夹中可能包含子文件夹,子文件夹中包含其他文件

public class CopyMultiFileFolder {
    public static void main(String[] args) throws IOException {
        // 创建数据源 File 目录对象
        File srcFile = new File("D:\\temp");
        // 创建目的 File 对象
        File destFile = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day06_File\\src\\itheima_11\\temp_1");

        // 调用方法复制文件夹中的内容
        copyFolder (srcFile, destFile);
    }

    // 复制文件夹方法
    private static void copyFolder(File srcFile, File destFile) throws IOException {
        // 判断是否是文件夹
        if (srcFile.isDirectory()) {
            // 在目的地创建和数据源 File 一样的文件名称
            String srcFileName = srcFile.getName();
            File newFolder = new File(destFile, srcFileName);
            if (! newFolder.exists()) {
                newFolder.mkdir();
            }
            // 获取数据源对象中的所有文件
            File[] listFiles = srcFile.listFiles();
            for (File listFile : listFiles) {
                copyFile(listFile, newFolder);
            }
        } else {
            // 不是文件夹,是文件,直接复制
            File newFile = new File(destFile, srcFile.getName());
            copyFile(srcFile, newFile);
        }

    }

    // 字节缓冲流复制文件
    private static void copyFile(File srcFile, File destFile) throws IOException {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));

        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        bos.close();
        bis.close();
    }
}
复制文件的异常处理

复制文件中,对于可能出现异常情况的处理方案:

一共有以下四种:

public class CopyFileException {

    public static void main(String[] args) throws IOException {
        method1();
        method2();
        method3();
        method4();
    }

    // 四、JDK9 对于 JDK7 方案的改进办法
    private static void method4 () throws IOException {
        // 此种写法最后会自动释放资源
        FileReader fr = new FileReader("fr.txt");
        FileWriter fw = new FileWriter("fw.txt");
        try (fr;fw) {
            char[] chars = new char[1024];
            int len;
            while ((len = fr.read(chars)) != -1) {
                fw.write(chars, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 三、JDK7 出现的改进方法
    private static void method3 () {
        // 此种写法最后会自动释放资源
        try (FileReader fr = new FileReader("fr.txt");
             FileWriter fw = new FileWriter("fw.txt");) {
            char[] chars = new char[1024];
            int len;
            while ((len = fr.read(chars)) != -1) {
                fw.write(chars, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 二、使用 Try-Catch 语句块捕获异常
    private static void method2 () {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            fr = new FileReader("fr.txt");
            fw = new FileWriter("fw.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }

        char[] chars = new char[1024];
        int len;
        try {
            while ((len = fr.read(chars)) != -1) {
                fw.write(chars, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            fw.close();
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 一、直接抛出处理
    private static void method1 () throws IOException {
        FileReader fr = new FileReader("fr.txt");
        FileWriter fw = new FileWriter("fw.txt");

        char[] chars = new char[1024];
        int len;
        while ((len = fr.read(chars)) != -1) {
            fw.write(chars, 0, len);
        }

        fw.close();
        fr.close();
    }
}

特殊操作流

标准输入输出流

System类中有两个标准的输入输出流,都是静态成员变量

  • public static final InputStream in:标准输入流。通常该流对应于键盘输入或由主机环境或用户指定的另一个输入源

  • public static final OutputStream out:标准输出流。通常该流对应于显示输出或由主机环境或用户指定的另一个输出目标

标准输入流

自己实现键盘录入数据:

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

调用Scanner类实现键盘录入数据:

Scanner sc = new Scanner(System.in);

实现代码:

public static void main(String[] args) throws IOException {
    // 使用多态的方式创建 标准输入流 对象
    /*InputStream is = System.in;
    // 字节流读取数据
    int by;
    while ((by = is.read()) != -1) {
        System.out.print((char) by);
    }
    // 释放资源
    is.close();*/

    // 上述代码不能实现中文的正常输出显示,转换为字符流实现
    /*InputStreamReader isr = new InputStreamReader(is);
    // 实现一行文字的读取,我们要转换成 字符缓冲流 实现
    BufferedReader br = new BufferedReader(isr);*/

    // 整合成一行代码,格式如下:
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    System.out.println("请输入一行文字:");
    String line = br.readLine();
    System.out.println(line);

    System.out.println("请输入一个整数:");
    int i = Integer.parseInt(br.readLine());
    System.out.println(i);

    // 但是上述实现过程太过复杂,我们直接使用 Scanner
    Scanner sc = new Scanner(System.in);
}

标准输出流

标准输出流本质上是PrintStream,也就是说PrintStream所具备的方法,System.out中也有,也可以直接调用

自己实现控制台打印输出:

public static void main(String[] args) {
    PrintStream ps = System.out;

    // 调用打印方法
    ps.print(100);
    ps.print("Hello World");

    // 调用换行打印
    ps.println(100);
    ps.println("Hello World");
    
    // 直接调用
    System.out.println(100);
    System.out.println("Hello World");
    
    // 换行打印方法可以无参数,但是 print 方法不能没有参数
    ps.println();
    // ps.print();
}
打印流

打印流只负责打印输出数据,不负责读取数据,有自己特有的方法

  • 字节打印流:PrintStream
  • 字符打印流:PrintWriter

字节打印流

创建字节打印流对象:

PrintStream ps = new PrintStream(fileName);

使用字节打印流写数据:

调用父类的方法write()会转码输入数据;如果调用自己特有方法print()/println()写数据,则会原样写入数据

public static void main(String[] args) throws FileNotFoundException {
    PrintStream ps = new PrintStream("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\print\\ps.txt");

    // 使用普通方法写数据
    ps.write(97); // 会自动转码成 ASCII 对应的字母 a

    // 使用特有的方法写数据
    ps.print(97);   // 直接写进的就是数字 97
    ps.println(98); // 换行写数据,末尾添加换行符
    ps.println(99);

    // 释放资源
    ps.close();
}

字符打印流

image-20210508200415410

第二种方式创建PrintWriter对象,会自动执行刷新,将缓冲区的数据读取出来

public static void main(String[] args) throws IOException {
    String fileName = "D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\print\\pw.txt";
    PrintWriter pw1 = new PrintWriter(fileName);
    // 写数据
    pw1.print("Hello1");
    pw1.println();
    pw1.flush();
    pw1.print("World1");
    // 释放资源,此步骤会实现自动刷新
    pw1.close();

    // 以这种方式创建字符输出流,会自动刷新
    PrintWriter pw2 = new PrintWriter(new FileWriter(fileName), true);
    // 写数据,此步骤不释放资源,自动刷新执行,数据写入
    pw2.println("Hello2");
    pw2.println("World2");
}
案例:复制Java文件(打印流实现)

需求:利用打印流实现Java文件的复制

public static void main(String[] args) throws IOException {
    /*// 根据数据源创建 缓冲输入流 对象
    BufferedReader br = new BufferedReader(new FileReader("day07_IOStream\\src\\print\\PrintStreamDemo.java"));
    // 根据目的地创建 缓冲输出流 对象
    BufferedWriter bw = new BufferedWriter(new FileWriter("day07_IOStream\\src\\print\\PrintStreamDemo_Copy.java"));
    // 读写数据,复制文件
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine();
        bw.flush();
    }
    // 释放资源
    bw.close();
    br.close();*/

    // 根据数据源创建 缓冲输入流 对象
    BufferedReader br = new BufferedReader(new FileReader("day07_IOStream\\src\\print\\PrintStreamDemo.java"));
    // 根据目的地创建 打印输出流 对象
    PrintWriter pw = new PrintWriter(new FileWriter("day07_IOStream\\src\\print\\PrintStreamDemo_Copy.java"), true);
    // 读写数据
    String line;
    while ((line = br.readLine()) != null) {
        pw.println(line);
    }
    // 释放资源
    pw.close();
    br.close();
}

相比于原方法,使用起来更加简单,执行更加高效

对象序列化流

image-20210508202806414

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o82ZXbH2-1639662744855)(https://i.loli.net/2021/05/08/t2Djv5xiSCcBKWY.png)]

对象序列化就是将对象的相关信息,存储到指定的文件中;等到需要重构对象的时候,再通过对文件调用对象反序列化流实现。

步骤一:在创建对象序列化对象的时候,首先要创建对象

public class Student implements Serializable {
    private String name;
    private int age;
    // 对应的无参构造和全参构造,以及 Getter and Setter 方法
}

步骤二:创建对象序列化流对象,将指定对象进行序列化

public class ObjectOutputStreamDemo {
    /*
    * NotSerializableException : 当一个实例需要实现Serializable接口。
    * 序列化运行时或实例类可以抛出此异常,参数应该是类的名称
    * Serializable : 一个类的串行化是由类实现java.io.serializable接口启用。
    * 类没有实现这个接口不会有任何序列化或反序列化其状态。
    * 序列化接口没有任何方法或字段只能识别可序列化的语义。
    * */
    public static void main(String[] args) throws IOException {
        // 创建 对象序列化流 对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
        // 创建对象
        Student s = new Student("雨下一整晚", 20);
        // 执行对象序列化
        oos.writeObject(s);
        // 释放资源
        oos.close();
    }
}

这个时候创建的新的oos.txt中的内容大部分是乱码,内容只有少部分信息可以看出。

这个时候我们要通过对象的反序列化流将oos.txt中的内容读取出来并创建相关的对象。

对象反序列化流

image-20210508210419862

需求:将之前的序列化后的对象文件实现 反序列化输出 创建原有的对象

/*
* ObjectInputStream(InputStream in) : 创建一个对象输入流读取从指定的输入流。
* */
public static void main(String[] args) throws IOException, ClassNotFoundException {
    // 创建对象反序列化流
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
    // 从已有的文件中读取数据,将对象反序列化
    // readObject() : 从对象输入流对象
    Object object = ois.readObject();
    // 转成对应的序列化之前的对象
    Student student = (Student) object;
    System.out.println(student.getName() + "," + student.getAge());
    // 释放资源
    ois.close();
}
相关问题

一、如果序列化后的类文件被修改之后,读取数据会不会出现问题?

类文件,也就是之前的Student类被修改,那么会不会出问题?

public class ObjectStreamDemo {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // write();
        read();
    }

    // 序列化
    private static void write () throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
        Student s = new Student("雨下一整晚Real", 20);
        oos.writeObject(s);
        oos.close();
    }

    // 反序列化
    private static void read () throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day07_IOStream\\src\\object_serialization_stream\\oos.txt"));
        Object object = ois.readObject();
        Student student = (Student) object;
        System.out.println(student.getName() + "," + student.getAge());
        ois.close();
    }
}

这个时候,我们首先调用write()方法,将对象进行序列化之后;我们修改Student类的代码,添加一个toString()方法,之后只调用read()方法,这个时候出现了异常

Exception in thread “main” java.io.InvalidClassException:
object_serialization_stream.Student;
local class incompatible: stream classdesc serialVersionUID = 1337395739814197595,
local class serialVersionUID = -2611514023222870104

当序列化运行时检测到一个类中的下列问题之一时抛出:

  • 类的串行版本与从流中读取的类的不匹配
  • 该类包含未知的数据类型
  • 类中没有一个可访问的无参数构造函数

通过观察异常产生的原因可以得知,是由于串行版本不一致导致的异常

序列化运行时与每个可序列化的类关联一个版本号,被称为serialVersionUID,用于在反序列化期间验证发送方和接收者有序列化对象,对象是相对于序列化兼容加载的类。如果接收者具有比相应的类的对象发送不同的serialVersionUID加载了一个类,然后反序列化将导致InvalidClassException

二、这种问题应该如何解决?

给序列化对象所属的类添加一个值:

private static final long serialVersionUID = 42L;

三、如果某个对象中的某个值不想被序列化,应该如何实现?

给该属性添加关键字 transient —— adj.短暂的;转瞬即逝的;倏忽;暂住的;过往的;临时的

// private int age;
private transient int age;
Properties

简单使用,和之前的集合的使用大致相同

注意:创建的时候不用写泛型

public static void main(String[] args) {
    // 创建集合对象
    Properties properties = new Properties();
    // 存储对象元素
    properties.put("001", "张三");
    properties.put("002", "李四");
    properties.put("003", "王五");
    // 遍历对象元素
    Set<Object> keySet = properties.keySet();
    for (Object key : keySet) {
        Object value = properties.get(key);
        System.out.println(key + "," + value);
    }
}

Properties作为集合的特有方法:

image-20210508223144530

public static void main(String[] args) {
    // 创建集合对象
    Properties prop = new Properties();
    // 调用 setProperties() 方法
    prop.setProperty("001", "张三");
    prop.setProperty("002", "李四");
    prop.setProperty("003", "王五");
    // 调用 getProperties() 方法
    String property = prop.getProperty("001");
    System.out.println(property);
    // 调用 stringPropertyNames() 获得键名
    Set<String> keySet = prop.stringPropertyNames();
    for (String value : keySet) {
        String s = prop.getProperty(value);
        System.out.println(value + "," + s);
    }
}
Properties和IO流结合的方法

image-20210508224042428

public class PropertiesDemo {
    public static void main(String[] args) throws IOException {
        // 把集合中的数据保存到文件
        myStore();
        // 把文件中的数据加载到集合
        myLoad();
    }

    private static void myLoad() throws IOException {
        Properties properties = new Properties();
        // 加载文件中的数据到集合中
        FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\Properties.txt");
        properties.load(fr);
        fr.close();
        System.out.println(properties);
    }

    private static void myStore() throws IOException {
        Properties properties = new Properties();
        // 往集合中添加数据
        properties.put("001", "张三");
        properties.put("002", "李四");
        properties.put("003", "王五");
        // 将集合中的数据写入文件
        FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\Properties.txt");
        properties.store(fw, null);
        fw.close();
    }
}
案例:游戏次数

需求:实现猜数字小游戏,每人只能玩三次;如果还想玩,提示:试玩已结束

image-20210508225458225

步骤一:创建游戏类 GuessNumber

public class GuessNumber {
    public GuessNumber() {
    }

    // 猜数字游戏
    public static void start () {
        // 要完成猜数字的游戏,首先要有一个要猜的数字,使用随机数生成,范围0-100
        Random random = new Random();
        int number = random.nextInt(100) + 1;

        while (true) {
            // 使用程序实现猜数字,每次均要实现键盘输入
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入你的答案:");
            int guessNumber = sc.nextInt();

            // 比较输入的数字和系统产生的数字的大小,根据大小输出相应的提示
            if (guessNumber > number) {
                System.out.println("你猜的数字" + guessNumber + "大了");
            } else if (guessNumber < number) {
                System.out.println("你猜的数字" + guessNumber + "小了");
            } else {
                System.out.println("恭喜你猜对了!");
                break;
            }
        }
    }
}

步骤二:创建game.txt文件

#Sat May 08 23:14:11 CST 2021
count=3

步骤三:编写判断主方法

public class PropertiesGuessNumber {
    public static void main(String[] args) throws IOException {
        // 创建集合对象
        Properties properties = new Properties();

        // 从文件中读取数据
        FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\game.txt");
        properties.load(fr);
        fr.close();

        // 通过 properties 集合获取到 count 的值
        String count = properties.getProperty("count");
        int number = Integer.parseInt(count);

        // 判断游戏运行的次数,并随着玩游戏的次数的增加,将 count 的值修改写入
        if (number >= 3) {
            // 如果次数到了,则提示试玩已结束
            System.out.println("试玩已结束!");
        } else {
            // 调用游戏开始方法
            GuessNumber.start();
            number ++;
            properties.setProperty("count", String.valueOf(number));
            // 创建 字符缓冲流 存储 count 值
            FileWriter fw = new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day07_IOStream\\src\\properties\\game.txt");
            properties.store(fw, null);
            fw.close();
        }
    }
}

当game.txt文件中的count = 3的时候,会提示“试玩已结束!”

七、多线程

实现多线程

进程和线程

进程:是系统正在运行的程序,

  • 系统进行资源分配和独立调用的基本单位;
  • 每一个进程都有它自己的内存空间和系统资源;

线程:是进程中的单个顺序控制流,是一条执行路径

  • 单线程:一个进程中只有一条执行路径,则称为单线程程序
  • 多线程:一个进程中如果有多条执行路径,则成为多线程程序
实现多线程

方式一:继承Thread

  1. 创建MyThread类继承Thread
  2. 重写Thread类中的run()方法
  3. 创建MyThread类对象
  4. 启动线程
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        // 直接调用run方法并没有启动多线程,需要调用start方法启动多线程
        /*myThread1.run();
        myThread2.run();*/
        myThread1.start();
        myThread2.start();
    }
}

注意:

一、为什么要重写run()方法?

因为run方法就是多线程在执行的时候需要被执行的内容,run()封装了被线程执行的代码

二、run()start()方法有什么区别?

run():封装线程被执行的代码,直接调用,相当于普通方法的调用

start():启动线程,然后由JVM调用该线程的run()方法

设置和获取线程名称
  • 设置线程名称方法 void setName(String name)将此线程的名称更改为参数中的值
  • 获取线程名称 String getName() 返回此线程的名称
  • 返回当前正在执行的线程对象的引用:public static Thread currentThread()
public class MyThread extends Thread{

    public MyThread() {

    }

    public MyThread(String name) {
        super(name);
    }

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



/*
* Thread中本身有一个名为name的成员变量
* private volatile String name;
*
无参构造方法
public Thread() {
    this(null, null, "Thread-" + nextThreadNum(), 0);
}
带参构造方法,可以在自己定义的类中添加无参构造之后再自己定义带参构造设置名字
public Thread(String name) {
    this(null, null, name, 0);
}
全参构造方法
public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize, boolean inheritThreadLocals) {
    this(group, target, name, stackSize, null, inheritThreadLocals);
}
获取线程名字方法
public final String getName() {
    return name;
}
设置名字方法
public final synchronized void setName(String name) {
        checkAccess();
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    if (threadStatus != 0) {
        setNativeName(name);
    }
}
初始化名字参数:
private static int threadInitNumber; 初始化值为0
private static synchronized int nextThreadNum() {
    return threadInitNumber++; 自动添加数值,返回当前值之后+1操作
}

* */
public class MyThreadDemo {
    public static void main(String[] args) {
        /*MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        mt1.setName("线程1");
        mt2.setName("线程2");
        mt1.start();
        mt2.start();*/

        MyThread mt1 = new MyThread("线程1");
        MyThread mt2 = new MyThread("线程2");

        mt1.start();
        mt2.start();

        // public static Thread currentThread()
        // 返回当前正在执行的线程对象的引用
        System.out.println(Thread.currentThread().getName());
    }
}
线程调度

线程调度有两种模式

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

Java所使用的是抢占式调度模型。所以多线程的程序执行具有随机性,因为谁抢占到CPU的使用权是不一定的。

Thread类中获取线程优先级以及设置线程优先级的方法:

  • public final int getPriority()返回此线程的优先级
  • public final void setPriority(int newPriority)更改此线程的优先级

线程优先级的范围是1-10,默认线程优先级是5;线程优先级高仅仅只是线程获得时间片的概率高,并不是线程一定能够每次都抢占到时间片。可能需要在多次运行之后,才能看到想要的结果。

public class ThreadPriority extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}
public class ThreadPriorityDemo {
    public static void main(String[] args) {
        ThreadPriority priority1 = new ThreadPriority();
        ThreadPriority priority2 = new ThreadPriority();
        ThreadPriority priority3 = new ThreadPriority();

        priority1.setName("飞机");
        priority2.setName("高铁");
        priority3.setName("火车");

        System.out.println(priority1.getPriority());  // 5
        System.out.println(priority2.getPriority());  // 5
        System.out.println(priority3.getPriority());  // 5


        // IllegalArgumentException : 如果优先级不在范围 MIN_PRIORITY到 MAX_PRIORITY
        // priority1.setPriority(10000);

        System.out.println(Thread.MIN_PRIORITY);    // 1
        System.out.println(Thread.MAX_PRIORITY);    // 10
        System.out.println(Thread.NORM_PRIORITY);   // 5

        // 线程优先级高仅仅表示获取到执行权限的概率更高,并不是每次都能获取执行
        priority1.setPriority(1);
        priority2.setPriority(5);
        priority3.setPriority(10);

        priority1.start();
        priority2.start();
        priority3.start();
    }
}
线程控制

image-20210522192936473

首先创建对应的线程类,代码如下所示:

public class ThreadSleep extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadSleepDemo {
    public static void main(String[] args) {
        ThreadSleep ts1 = new ThreadSleep();
        ThreadSleep ts2 = new ThreadSleep();
        ThreadSleep ts3 = new ThreadSleep();

        ts1.setName("曹操");
        ts2.setName("刘备");
        ts3.setName("孙权");

        ts1.start();
        ts2.start();
        ts3.start();
    }
}

Thread.sleep()的作用是让线程进行休眠,参数为指定的休眠时间。

创建线程类ThreadDemo,之后通过创建实例对象,对两个方法进行验证使用。

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

Thread.join()方法,是等待线程结束。

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadJoin tj1 = new ThreadJoin();
        ThreadJoin tj2 = new ThreadJoin();
        ThreadJoin tj3 = new ThreadJoin();

        tj1.setName("Join1");
        tj2.setName("Join2");
        tj3.setName("Join3");

        tj1.start();
        // join() 等待该线程死亡
        tj1.join();
        tj2.start();
        tj3.start();
    }
}

Thread.setDaemon()设置守护线程,当前所运行的线程全为守护线程的时候,Java虚拟机将退出。

public class ThreadDaemonDemo {
    public static void main(String[] args) {
        ThreadDaemon td1 = new ThreadDaemon();
        ThreadDaemon td2 = new ThreadDaemon();

        td1.setName("关羽");
        td2.setName("张飞");

        // 设置主线程
        Thread.currentThread().setName("刘备");

        // 设置守护线程,在主线程结束之后立刻结束
        // setDaemon(boolean on) 标志着该线程是daemon线程或用户线程
        // 当运行的线程都是守护线程的时候,Java虚拟机将退出
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
线程生命周期

image-20210524003100680

多线程的实现方法

有两种方法来创建一个新的执行线程,一是声明一个类是一类Thread。这类应重写类Thread的run方法,子类的一个实例可以被分配和启动。创建一个线程的另一个方式是声明一个类实现Runnable接口,该类实现run方法。然后可以分配该类的实例,在创建Thread时作为参数传递并启动。

方式二:实现Runnable接口

  • 定义一个类MyRunnable实现Runnable接口
  • 在MyRunnable类中重写run方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,将MyRunnable对象作为构造方法的参数
  • 启动线程
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        // Thread(Runnable target) 分配一个新的 Thread 对象
        Thread tr1 = new Thread(mr);
        Thread tr2 = new Thread(mr);
        /*tr1.start();
        tr2.start();*/

        // Thread(Runnable target, String name) 分配一个新的 Thread对象
        Thread tr3 = new Thread(mr, "火车");
        Thread tr4 = new Thread(mr, "高铁");
        tr3.start();
        tr4.start();
    }
}

相比于直接继承自Thread类,实现Runnable接口的好处:

  • 避免了Java的单继承,在实现多线程的时候还可以再继承自另一个接口或者类
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好地体现了面向对象的设计思想

线程同步

案例:多窗口售卖电影票,总票数100

image-20210524172442855

实现代码如下所示:

public class SellTicket implements Runnable {

    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread th1 = new Thread(st, "窗口1");
        Thread th2 = new Thread(st, "窗口2");
        Thread th3 = new Thread(st, "窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}
思考

现实生活中,卖票也是需要时间的;反映在程序中,我们给每一次的卖票过程中添加一个Sleep方法,每一次卖票让线程休息100ms。

修改run方法,如下:

@Override
public void run() {
    while (true) {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
            tickets--;
        }
    }
}

这样引发的运行结果会出现问题:①同一张的票出现多次;②出现负数编号的票

窗口1正在出售第100张票
窗口3正在出售第100张票
窗口2正在出售第100张票

窗口3正在出售第0张票
窗口2正在出售第-1张票

问题的原因主要是线程执行的随机性,分析过程如下:

image-20210524174413883

线程数据安全

上述案例中的ticket变量之所以会出现不合理的情况,是因为同一时刻被多个线程所访问,导致数据被不合理修改。

判断数据安全:

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

解决数据安全问题:

  • 设计思想:让程序没有安全问题的环境
  • Java提供了同步代码块的解决方式

锁多条代码块操作共享数据,可以使用同步代码块实现:

synchronized (任意对象) {
    // 多条语句操作共享数据代码
}

相当于给代码块内部的代码加锁,任意对象可以看成是一把锁。

public class SellTicket implements Runnable {

    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }
    
}

最好的加锁方式就是重新定义一个对象,传输到任意对象的位置;这样当不同线程使用的时候,就会默认变成加了不同的锁,从而实现加锁的目的。

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多的时候,每次线程在运行同步代码块之前都需要判断上锁的状态,这是很耗费资源的,会拖累运行效率
同步方法

同步方法就是在方法上添加关键字 synchronized ,加锁的对象是this

同步方法格式:

private synchronized void methodName() {}

同步静态方法就是在静态方法上添加关键字 synchronized,加锁的对象是 类名.class

同步静态方法格式:

private static synchronized void methodName() {}
线程安全的类

StringBuffer

  • 线程安全的可变序列。
  • 从JDK 5开始,被StringBuilder替代。通常应该使用StringBuilder,因为它支持所有相同的操作不执行同步,执行速度更快。

Vector

  • 从Java 2平台v1.2开始,该类改进了List接口。与新的集合实现不同,它实现了同步,这意味着它是线程安全的。如果不需要线程同步,建议使用ArrayList对象。

Hashtable

  • 该类实现了一个Hash表,他将键映射到值。任何非NULL对象都可以用作键或者值。
  • 从Java 2平台v1.2开始,该类改进了Map接口。与新的集合实现不同,它实现了同步,这意味着它是线程安全的。如果不需要线程同步,建议使用HashMap对象。

上述对应的线程安全类都有其对应的普通实现类,实例如下:

public static void main(String[] args) {
    StringBuffer sb1 = new StringBuffer();
    StringBuilder sb2 = new StringBuilder();

    Vector<String> vector = new Vector<String>();
    ArrayList<String> arrayList = new ArrayList<String>();

    Hashtable<String, String> hashtable = new Hashtable<String, String>();
    HashMap<String, String> hashMap = new HashMap<String, String>();

    // synchronizedList(List<T> list) 返回由指定列表支持的同步(线程安全)列表
    List<String> strings = Collections.synchronizedList(new ArrayList<String>());
}
public static void main(String[] args) {
    StringBuffer sb1 = new StringBuffer();
    StringBuilder sb2 = new StringBuilder();

    Vector<String> vector = new Vector<String>();
    ArrayList<String> arrayList = new ArrayList<String>();

    Hashtable<String, String> hashtable = new Hashtable<String, String>();
    HashMap<String, String> hashMap = new HashMap<String, String>();

    // synchronizedList(List<T> list) 返回由指定列表支持的同步(线程安全)列表
    List<String> strings = Collections.synchronizedList(new ArrayList<String>());
}

VectorHashtable现在已经不常用了,经常已经被后面的形式所替代。

Lock锁

Lock锁提供了比synchronized同步块更为广泛的锁操作,可以实现更多复杂的锁操作。

Lock中提供了获得锁和释放锁的操作:

  • void lock() 获得锁
  • void unlock() 释放锁

其中Lock是一个接口,不能直接实例化,我们使用到它的具体实现类ReentrantLock来进行实例化。

构造方法:

public ReentrantLock() {}  // 获得一个ReentrantLock的实例

案例:卖票案例,利用Lock锁对象来实现

public class SellTicket implements Runnable{
    private int tickets = 100;
    // 创建锁的对象
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

在这个过程中,在lock()unlock()之间的代码就会默认是上锁的代码。作用效果和synchronized是一样的,但是为了防止上锁的代码部分在执行的过程中出现问题,我们将unlock()的调用放到finally代码块中,这样我们才能保证整个程序在运行的过程中不会出现问题。

生产者消费者问题

生产者消费者问题,实际上就是两类线程的问题:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据

用于解耦生产者和消费者之间的关系,通常会采用一个共享数据的区域,我们通常把它看成是一个仓库

  • 生产者生产数据之后直接放在共享的数据区域中,并不需要关心消费者的行为
  • 消费者只需要从共享区域中获取到共享的数据,并不需要关心生产者的行为

为了体现生产者和消费者之间的等待和唤醒,Java中提供了几个方法供我们使用:

image-20210525130816145

实现案例:

牛奶生产者和消费者,通过一个存放牛奶的奶箱实现两者的交流

image-20210525131042096

开发步骤:

①创建生产者类Producer

public class Producer implements Runnable{
    private Box box;

    public Producer(Box box) {
        this.box = box;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            box.put(i);
        }
    }
}

②创建消费者类对象Customer

public class Customer implements Runnable{
    private Box box;
    public Customer(Box box) {
        this.box = box;
    }

    @Override
    public void run() {
        while (true) {
            box.get();
        }
    }
}

③创建共享数据对象Box

public class Box {
    // 定义一个成员变量,表示是第几瓶奶
    private int milk;
    // 定义一个成员变量,表示奶箱的状态
    private boolean state = false;
    // 定义一个存储牛奶以及获取牛奶的方法
    public synchronized void put(int milk) {
        // 如果存在牛奶,等待消费
        if (state) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果没有牛奶,则生产牛奶
        this.milk = milk;
        System.out.println("送奶工将第" + this.milk + "瓶奶送到");
        // 生产牛奶完毕,修改奶箱状态
        state = true;
        // 唤醒其他等待的线程
        notifyAll();

    }
    public synchronized void get() {
        // 如果没有牛奶,等待生产
        if (!state) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果存在牛奶,进行消费
        System.out.println("消费者将第" + this.milk + "瓶奶取走");
        // 消费完毕,修改奶箱状态
        state = false;
        // 唤醒其他线程
        notifyAll();
    }
}

④创建操作实现类BoxDemo

public class BoxDemo {
    public static void main(String[] args) {
        // 创建奶箱对象,表示这是共享数据区
        Box box = new Box();

        // 创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
        Producer p = new Producer(box);
        // 创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用取走牛奶的操作
        Customer c = new Customer(box);

        // 创建两个线程,分别把生产者和消费者对象作为参数传递
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        // 启动线程
        t1.start();
        t2.start();
    }
}

最终的运行结果:

送奶工将第1瓶奶送到
消费者将第1瓶奶取走
送奶工将第2瓶奶送到
消费者将第2瓶奶取走
送奶工将第3瓶奶送到
消费者将第3瓶奶取走
送奶工将第4瓶奶送到
消费者将第4瓶奶取走
送奶工将第5瓶奶送到
消费者将第5瓶奶取走

八、网络编程

概述

image-20210525135210984

网络编程

  • 在网络通信协议下,实现网络互连的不同计算机上运行的程序间可以进行数据交换
网络编程三要素

image-20210525135553182

IP地址

IP地址是网络中设备的唯一标识,IP地址分为两大类

image-20210525135716104

常用命令

  • ipconfig 用来查看本机IP地址相关信息
  • ping ip地址 检查网络的连通信

特殊地址

127.0.0.1 回送地址,可以代表本机地址,一般用来测试使用

InetAddress

为了方便网络编程,Java提供了InetAddress类用来获取IP地址

InetAddress类表示Internet协议(IP)地址

image-20210525140730850

使用案例:

public static void main(String[] args) throws UnknownHostException {
    // InetAddress address = InetAddress.getByName("DESKTOP-6QQI4OP");
    InetAddress address = InetAddress.getByName("192.168.123.231");

    // 获取IP地址的主机名
    String hostName = address.getHostName();
    System.out.println("主机名:" + hostName);

    // 获取IP地址
    String ip = address.getHostAddress();
    System.out.println("IP地址:" + ip);
}
端口

端口:设备上应用程序的唯一标识

端口号:用两个字节表示的整数,取值范围值0-65535;其中,0-1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前应用启动失败。

协议

协议:计算机网络中,连接和通信的规则被称为网络通信协议

image-20210525142344571

image-20210525142538654

三次握手示意图

image-20210525142625268

TCP和UDP

UDP通信程序

UDP是一种不可靠的网络传输协议,他在通信两端各建立一个Socket对象,但是这两个Socket只是发送/接收数据的对象;因此对基于UDP通信协议的双方而言,没有所谓的客户端服务器的概念。

Java提供了DatagramSocket类作为基于UDP协议的Socket

发送数据的步骤

①创建发送端的Socket对象DatagramSocket

②创建数据,并把数据打包

③调用DatagramSocket对象的方法发送数据

④关闭发送端

public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 创建发送端的 Socket 对象 DatagramSocket
        // DatagramSocket() 构建一个数据报套接字绑定到本地主机的任何可用的端口
        DatagramSocket ds = new DatagramSocket();

        // 创建数据,并把数据打包
        // DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
        // 构造一个指定长度的数据包,发送到指定主机上的指定端口号
        byte[] bytes = "Hello, World".getBytes();
        /*int length = bytes.length;
        InetAddress address = InetAddress.getByName("192.168.123.231");
        int port = 10010;
        DatagramPacket dp = new DatagramPacket(bytes, length, address, port);*/
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.123.231"), 10010);

        // 调用 DatagramSocket 对象的方法发送数据
        // void send(DatagramPacket p)  从这个套接字发送数据报包
        ds.send(dp);

        // 关闭发送端
        // void close() 关闭该数据报套接字
        ds.close();
    }
}

接收数据的步骤

①创建一个接收端的Socket对象用于接收数据(DatagramSocket

②创建一个数据包用于接收数据

③调用DatagramSocket的方法用于接收数据

④解析数据包,并在控制台打印数据

⑤关闭接收端

public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // ①创建一个接收端的Socket对象用于接收数据(DatagramSocket)
        DatagramSocket ds = new DatagramSocket(10010);

        // ②创建一个数据包用于接收数据
        // DatagramPacket(byte[] buf, int length)  构造一个DatagramPacket用于接收数据包长度为 length 的数据包
        byte[] bytes = new byte[1024]; // 实际数据长度大小可能并没有这么多
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

        // ③调用 DatagramSocket 的方法用于接收数据
        ds.receive(dp);

        // ④解析数据包,并在控制台打印数据
        // byte[] getData() 返回数据缓冲区
        byte[] data = dp.getData();
        // int getLength()  返回要发送的数据的长度或收到的数据的长度
        int length = dp.getLength();
        String dataStr = new String(data, 0, length);
        System.out.println(dataStr);

        // ⑤关闭接收端
        ds.close();
    }
}

运行时,先运行接收端,接收端会一直开启等待数据发送;之后运行发送端,发送端发送数据由接收端接收之后,接收端会执行相关操作,最后在控制台打印输出相关的数据。

image-20210525150404039

练习:UDP通信

按下面要求实现程序:

  • UDP发送数据:数据来自于键盘输入,直到输入的数据是886,发送数据结束
  • UDP接收数据:数据来自于发送程序,因为不知道什么时候停止接收数据,故采用死循环接收

发送端程序:

public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 从键盘录入数据进行发送,直到录入的数据是886,停止录入
        // 创建发送端的Socket对象 DatagramSocket
        DatagramSocket ds = new DatagramSocket();

        // 自己封装一个键盘录入
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line;
        while ((line = br.readLine()) != null) {
            // 判断数据是否是 886
            if ("886".equals(line)) {
                break;
            }
            // 创建发送端的数据包对象
            byte[] bytes = line.getBytes();
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.123.231"), 10086);

            // 调用DatagramSocket对象的相关方法进行发送
            ds.send(dp);
        }

        // 关闭发送端
        ds.close();
    }
}

接收端程序:

public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // 创建接收端对象
        DatagramSocket ds = new DatagramSocket(10086);
        while (true) {
            // 调用 DatagramSocket对象的接收方法
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
            // 接收数据,对数据进行解析
            ds.receive(dp);
            String data = new String(dp.getData(), 0, dp.getLength());
            System.out.println(data);
            // 关闭接收端,死循环接收数据,无操作
        }
    }
}
TCP通信程序

TCP通信协议是一个可靠的网络通信协议。它在通信的两端各建立一个Socket对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。
Java对基于TCP协议的网络通信提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
Java为客户端提供了Socket类,为服务器端提供了ServerSocket类。

TCP发送数据

步骤:

①创建客户端的Scoket对象

②获取输出流,写数据

③释放资源

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 创建Scoket对象
        // Socket(InetAddress address, int port) 创建一个流套接字,并将其与指定的IP地址中的指定端口号连接起来
        // Socket s = new Socket(InetAddress.getByName("192.168.123.231"), 10000);
        // Socket(String host, int port) 创建一个流套接字,并将其与指定的主机上的指定端口号连接起来
        Socket s = new Socket("192.168.123.231", 10000);

        // 获取输出流,写数据
        // OutputStream getOutputStream() 返回此套接字的输出流
        OutputStream os = s.getOutputStream();
        os.write("Hello, World!".getBytes());

        // 释放资源
        os.close();
        s.close();
    }
}

TCP接收数据

步骤:

①创建服务器端的Socket对象(ServerSocket

②监听客户端连接,并返回Socket对象

③获取输入流,读数据,并把数据显示输出在控制台

④释放资源

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 创建服务器端的Socket对象(`ServerSocket`)
        ServerSocket ss = new ServerSocket(10010);

        // 获取输入流,读数据,并把数据显示输出在控制台
        // Socket accept() 监听要对这个套接字作出的连接并接受它
        Socket s = ss.accept();
        InputStream is = s.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        String data = new String(bytes, 0, len);
        System.out.println(data);

        // 释放资源
        s.close();
        ss.close();

    }
}

运行时先运行服务器端程序,之后再运行客户端程序;由服务器端程序监听连接状态,客户端程序发送连接请求,通过TCP协议进行连接通信,之后各自分别进行数据的发送和接收。

练习:TCP通信

案例一:需求如下:

  • 客户端:发送数据,接收服务器端反馈
  • 服务器端:接收数据,给出反馈
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocket对象
        ServerSocket ss = new ServerSocket(10010);

        // 监听连接,得到Socket对象
        Socket s = ss.accept();
        InputStream is = s.getInputStream();
        byte[] bytes = new byte[1024];

        // 读取数据,释放资源
        int len = is.read(bytes);
        String data = new String(bytes, 0, len);
        System.out.println("服务器端:" + data);

        // 给客户端发出反馈
        OutputStream os = s.getOutputStream();
        os.write("数据已成功发送".getBytes(StandardCharsets.UTF_8));

        // 释放资源
        ss.close();
    }
}
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 首先创建Socket对象
        Socket s = new Socket("192.168.123.231", 10010);

        // 获取输出流,写数据
        OutputStream os = s.getOutputStream();
        os.write("Hello TCP Server".getBytes());

        // 接收服务器端的反馈
        InputStream is = s.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        String data = new String(bytes, 0, len);
        System.out.println("客户端:" + data);

        // 释放资源
        s.close();
    }
}

案例二:需求如下所示,要求运用TCP协议

  • 客户端:数据来自于键盘,直到输入的数据的数字是886,输入结束
  • 服务器端:数据来自于客户端,将客户端的数据显示在控制台
public class ClientInputData {
    public static void main(String[] args) throws IOException {
        // 创建一个 Socket 对象
        Socket s = new Socket("192.168.123.231", 10010);

        // 从键盘读取数据,一直到读取到特定字符结束读取
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 封装输出流对象
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            if ("886".equals(line)) {
                break;
            }
            // 获取输出流,写数据
            /*OutputStream os = s.getOutputStream();
            os.write(line.getBytes(StandardCharsets.UTF_8));*/
            bw.write(line);
            bw.newLine();
            bw.flush();
        }

        // 释放资源
        s.close();
    }
}
public class ServerInputData {
    public static void main(String[] args) throws IOException {
        // 创建一个 ServerSocket 对象
        ServerSocket ss = new ServerSocket(10010);

        // 监听连接,获取 Socket 对象
        Socket s = ss.accept();
        /*InputStream is = s.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);*/
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }

        // 释放资源
        ss.close();
    }
}

案例三:需求如下:

  • 客户端:数据来自于文本文件,接收服务器反馈
  • 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程

①创建线程类

public class ServerThread implements Runnable {
    private Socket s;
    public ServerThread(Socket s) {
        this.s = s;
    }

    @Override
    public void run() {
        // 接收数据写到文本文件
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
            // BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy.java"));
            // 解决名称问题
            int count = 0;
            File file = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy(" + count + ").java");
            while (file.exists()) {
                count++;
                file = new File("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\Copy(" + count + ").java");
            }
            BufferedWriter bw = new BufferedWriter(new FileWriter(file));
            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
                bw.flush();
            }

            // 给出反馈
            BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
            bwServer.write("文件上传成功!");
            bwServer.newLine();
            bwServer.flush();

            // 释放资源
            s.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

②创建服务器类

public class ThreadServer {
    // 服务器:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程
    public static void main(String[] args) throws IOException {
        // 创建服务器 Socket 对象
        ServerSocket ss = new ServerSocket(10010);

        // 监听服务器连接,获取Socket对象
        while (true) {
            Socket s = ss.accept();
            new Thread(new ServerThread(s)).start();
        }

        // 不需要关闭服务器
    }
}

③创建客户端类

public class ThreadClient {
    public static void main(String[] args) throws IOException {
        // 创建客户端 Socket 对象
        Socket s = new Socket("192.168.123.231", 10010);

        // 封装上传的文本文件
        BufferedReader br = new BufferedReader(new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day14_NetPrograming\\src\\TCP_exercise\\ThreadClient.java"));
        // 封装输出流写数据
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine();
            bw.flush();
        }

        s.shutdownOutput();

        // 接收反馈
        BufferedReader brClient = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String read = brClient.readLine();
        System.out.println(read);

        // 释放资源
        br.close();
        s.close();
    }
}

九、Lambda表达式

函数式编程思想概述

面向对象的思想强调:”必须通过对象的形式来工作“
函数式编程思想则尽量忽略面向对象的复杂思想;”强调做什么,而不是以什么形式去做“

我们学习的Lambda表达式就是以函数式编程思想的一种体现,jdk 8 新特性。

Lambda表达式

体验

需求:启动一个线程,在控制台输出一句话:多线程程序启动了

public class LambdaDemo {
    public static void main(String[] args) {
        // 实现类的方式实现
        /*MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr);
        t1.start();*/
        // 匿名内部类的方式实现
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多线程程序启动了");
            }
        }).start();*/
        // Lambda 表达式的方式实现
        new Thread( () -> {
            System.out.println("多线程程序启动了");
        }).start();
    }
}

我们提供了三种方式实现这种需求,其中Lambda表达式的方式最为简便高效。

Lambda表达式的标准格式

image-20210526014306386

三要素:形式参数、箭头、代码块

(formal parameters) -> { 
    // code
}
  • 形式参数如果有多个,中间用逗号隔开;如果没有参数,留空即可。
  • ->由英文的中划线加大于号组成,是一种固定写法,代表指向动作
  • 代码块是我们要做的具体内容,也就是我们之前的方法体内容
  • 使用前提:①有一个接口;②接口中有且仅有一个抽象方法

Lambda表达式练习

案例一:

  • 定义一个接口Eatable,里面定义一个方法:void eat();
  • 定义一个测试类,在测试类中提供两个方法:
    • 一个方法是useEatable(Eatable e)
    • 另一个方法是主方法,在主方法中调用useEatable方法

①创建接口类

public interface Eatable {
    void eat();
}

②创建接口对应的实现类

public class EatableImpl implements Eatable{
    @Override
    public void eat() {
        System.out.println("一天一苹果,医生远离我");
    }
}

③创建测试类,用三种方法实现调用

public class EatableDemo {
    public static void main(String[] args) {
        Eatable e = new EatableImpl();
        useEatable(e);

        // 匿名内部类
        useEatable(new Eatable() {
            @Override
            public void eat() {
                System.out.println("一天一苹果,医生远离我");
            }
        });

        // Lambda 表达式的使用
        useEatable(() -> {
            System.out.println("一天一苹果,医生远离我");
        });
    }

    private static void useEatable(Eatable e) {
        e.eat();
    }
}

案例二:

  • 定义一个接口Flyable,里面定义一个方法:void fly(String s);
  • 定义一个测试类,在测试类中提供两个方法:
    • 一个方法是useFlyable(Flyable f)
    • 另一个方法是主方法,在主方法中调用useFlyable方法

①定义一个接口

public interface Flyable {
    void fly(String s);
}

②创建测试类

public class FlyableDemo {
    public static void main(String[] args) {
        // 匿名内部类实现
        useFlyable(new Flyable() {
            @Override
            public void fly(String s) {
                System.out.println(s);
                System.out.println("匿名内部类");
            }
        });

        // Lambda 表达式
        useFlyable( (String s) -> {
            System.out.println(s);
            System.out.println("Lambda表达式");
        });
    }
    private static void useFlyable(Flyable f) {
        f.fly("风和日丽,晴空万里");
    }
}

案例三:

  • 定义一个接口Addable,里面定义一个方法:int add(int x,int y);
  • 定义一个测试类,在测试类中提供两个方法:
    • 一个方法是useAddable(Addable a)
    • 另一个方法是主方法,在主方法中调用useAddable方法

①创建接口

public interface Addable {
    int add(int x, int y);
}

②创建测试类

public class AddableDemo {
    public static void main(String[] args) {
        // 匿名内部类实现
        useAddable(new Addable() {
            @Override
            public int add(int x, int y) {
                return x + y;
            }
        });

        // Lambda 表达式
        useAddable( (int x, int y) -> {
            return x + y;
        });
    }
    private static void useAddable(Addable a) {
        int sum = a.add(10, 20);
        System.out.println(sum);
    }
}

Lambda表达式的省略模式

省略模式:

  • 参数类型可以省略,但是有多个参数的时候,不能只省略部分
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,甚至是return

调用之前编写的接口,尝试Lambda表达式的省略模式的运用:

public class LambdaDemo {
    public static void main(String[] args) {
        useAddable((int x, int y) -> {
            return x + y;
        });
        // 省略模式:参数的类型可以省略
        // 但是有多个参数的情况下,不能只省略部分
        useAddable((x, y) -> {
            return x + y;
        });
        useFlyable((s) -> {
            System.out.println(s);
        });
        // 省略模式:如果参数仅有一个,小括号()可以省略
        useFlyable(s -> {
            System.out.println(s);
        });
        // 省略模式:如果代码块的语句只有一条,可以省略大括号{}和代码块语句的分号;
        useFlyable(s -> System.out.println(s));
        // 省略模式:如果代码块的语句只有一条,可以省略大括号{}和代码块语句的分号; 如果有 return 语句 ,return 也要省略掉
        useAddable((x, y) -> x + y);
    }

    private static void useAddable(Addable addable) {
        int sum = addable.add(10, 20);
        System.out.println(sum);
    }

    private static void useFlyable(Flyable flyable) {
        flyable.fly("风和日丽,晴空万里");
    }
}

Lambda表达式的注意事项

注意事项:

  • 使用Lambda必须要有一个接口,且接口中有且仅有一个抽象方法

  • 使用Lambda必须要有上下文环境,才能推导出Lambda对应的接口

    • // 根据局部变量的赋值得知
      Runnable r = () -> System.out.println("Lambda表达式");
      new Thread(r).start();
      
    • // 根据调用方法的参数得知
      new Thread(() -> System.out.println("Lambda表达式")).start();java
      

测试类:

public class LambdaDemo {
    public static void main(String[] args) {
        // 注意:使用Lambda必须要有一个接口,且接口中有且仅有一个抽象方法
        useInter(() -> System.out.println("好好学习,天天向上"));
        // 注意:使用Lambda必须要有上下文环境,才能推导出Lambda对应的接口
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类");
            }
        }).start();
        // 根据局部变量的赋值得知
        Runnable r = () -> System.out.println("Lambda表达式");
        new Thread(r).start();
        // 根据调用方法的参数得知
        new Thread(() -> System.out.println("Lambda表达式")).start();

    }
    private static void useInter(Inter i) {
        i.show();
    }
}

Lambda表达式和匿名内部类的区别

  • 所需类型不同
    • 匿名内部类:可以是接口,可以是抽象类,也可以是具体类
    • Lambda表达式:只能是接口,而且接口中只能有一个抽象方法
  • 使用限制不同
    • 如果一个接口中有且仅有一个抽象方法,可以使用匿名内部类,也可以使用Lambda表达式
    • 如果一个接口中存在多个抽象方法,只能使用匿名内部类
  • 实现原理不同
    • 匿名内部类:编译之后,会在磁盘中产生一个单独的.class字节码文件
    • Lambda表达式:编译之后,没有一个单独的.class字节码文件,对应的字节码会在运行的时候动态生成

匿名内部类在编译之后会在内存中产生一个单独的.class字节码文件

public class LambdaDemo {
    public static void main(String[] args) {
        // 匿名内部类
        /*useInter(new Inter() {
            @Override
            public void show() {
                System.out.println("接口");
            }
        });
        useAnimal(new Animal() {
            @Override
            void method() {
                System.out.println("抽象类");
            }
        });
        useStudent(new Student() {
            @Override
            void study() {
                System.out.println("具体类");
            }
        });*/

        // Lambda
        useInter(() -> System.out.println("接口"));
        // Lambda只支持接口类型,且接口中仅有一个抽象方法
        /*useAnimal(() -> System.out.println("抽象类"));
        useStudent(() -> System.out.println("具体类"));*/
    }
    private static void useInter(Inter i) {
        i.show();
    }
    private static void useAnimal(Animal a) {
        a.method();
    }
    private static void useStudent(Student s) {
        s.study();
    }
}

十、反射

类加载

当程序要使用某个类时,如果该类还没有被加载到内存中时,该系统会通过类的加载、类的连接、类的初始化三个步骤来对类进行初始化。如果没有出现意外,JVM会连续完成这三个步骤,所以有时也将这三个步骤统称为类加载或类初始化。

类的加载

  • 类加载就是将class文件读入内存,并为之创建一个java.lang.Class的对象
  • 任何类被使用时,系统都会为之建立一个Java.lang.Class对象

类的连接

  • 验证阶段:用于检验被加载的类是否具有正确的内部结构,和其他类是否协调一致
  • 准备阶段:负责为类的类变量分配内存,并设置默认初始化值
  • 解析阶段:将类的二进制数据中的符号引用替换为直接引用

类的初始化

  • 在该阶段,主要就是对类变量进行初始化

类的初始化步骤

  • 假如该类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

注意:在执行第二个步骤的时候,如果该类还有直接父类,则也依次按照上述步骤执行
所以,根据这个逻辑,最先被初始化完成的是java.lang.Object类

类的初始化时机

  • 创建类的实例
  • 调用类的类方法
  • 访问类或者类接口的类变量,或者为该类变量赋值
  • 使用反射方式来强制创建某个类或者接口对应的java.lang.Class对象
  • 初始化某个类的子类
  • 直接使用java.exe命令来运行某个主类
类加载器

作用:

  • 负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象
  • 虽然不用过分关注类加载机制,但是了解类加载机制我们能够更好地了解整个程序的运行

JVM的类加载机制

  • 全盘负责:就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托:就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

ClassLoader:负责加载类的对象

Java运行时有以下内置的加载器

  • Bootstrap class loader:它是虚拟机的内置类加载器,通常表示为null,并且没有父null
  • Platform class loader:平台类加载器可以看到所有平台类,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类
  • System class loader:它也被称为应用程序类加载器,与平台类加载器不同。系统类加载器通常用于定义应用程序类路径,模块路径和JDK特定工具上的类
  • 类加载器的继承关系:System的父加载器为Platform,而Platform的父加载器为Bootstrap

ClassLoader中的两个方法:

  • static ClassLoader getSystemClassLoader():返回用于委派的系统类加载器
  • ClassLoader getParent():返回父类加载器进行委派

案例代码:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // static ClassLoader getSystemClassLoader() :返回用于委派的系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        // jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
        System.out.println(systemClassLoader);
        // ClassLoader getParent() :返回父类加载器进行委派
        ClassLoader parent1 = systemClassLoader.getParent();
        // jdk.internal.loader.ClassLoaders$PlatformClassLoader@10f87f48
        System.out.println(parent1);
        ClassLoader parent2 = parent1.getParent();
        // null Bootstrap类加载器,只是因为表示为null
        System.out.println(parent2);
    }
}

反射

反射概述

Java反射机制:是指在运行时去获取一个类的变量和方法信息。然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展

image-20210528134700143

获取Class类的对象

我们要想通过反射去使用一个类,首先我们要获取到该类的字节码文件对象,也就是类型为Class类型的对象

这里我们提供三种方式获取Class类型的对象

  • 使用类的class属性来获取该类对应的Class对象。举例:Student.class将会返回Student类对应的Class对象
  • 调用对象的getClass()方法,返回该对象所属类对应的Class对象
    • 该方法是Object类中的方法,所有的Java对象都可以调用该方法
  • 使用Class类中的静态方法forName(String className),该方法需要传入字符串参数,该字符串参数的值是某个类的全路径,也就是完整包名的路径

案例:

一、首先创建Student类

public class Student {
    // 提供三个变量:一个私有,一个默认,一个公共
    private String name;
    int age;
    public String address;
    // 提供两个构造方法:一个私有,一个默认,两个公共
    public Student() {

    }
    private Student(String name) {
        this.name = name;
    }
    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    // 成员方法:一个私有,四个公共
    private void function() {
        System.out.println("function");
    }
    public void method1() {
        System.out.println("method");
    }
    public void method2(String s) {
        System.out.println("method" + s);
    }
    public String method3(String s, int i) {
        return s + "," + i;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

二、创建测试类

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 使用类的class属性来获取该类对应的Class对象
        Class<Student> c1 = Student.class;
        System.out.println(c1);
        Class<Student> c2 = Student.class;
        System.out.println(c1 == c2);
        // 调用对象的 getClass() 方法
        Student s = new Student();
        Class<? extends Student> c3 = s.getClass();
        System.out.println(c3 == c1);
        // 使用Class类中的静态方法 forName(String className)
        Class<?> c4 = Class.forName("get_class.Student");
        System.out.println(c4 == c1);
    }
}

运行结果:

class get_class.Student
true
true
true

反射获取构造方法并调用

Class类中用于获取构造方法的方法

  • Constructor<?>[] getConstructors():返回所有公共构造方法对象的数组
  • Constructor<?>[] getDeclaredConstructors():返回所有构造方法对象的数组
  • Constructor<T> getConstructor(Class <?> ... parameterTypes):返回单个公共构造方法对象
  • Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes):返回单个构造方法对象

Constructor类中用于创建对象的方法

  • T newInstance(Object... initargs):根据指定的构造方法创建对象

案例:利用上面创建的Student类进行测试

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 使用静态方法获取上次使用的学生类
        Class<?> aClass = Class.forName("get_class.Student");
        // Constructor<?>[] getConstructors() :返回所有公共构造方法对象的数组
        // Constructor<?>[] constructors = aClass.getConstructors();
        // Constructor<?>[] getDeclaredConstructors() :返回所有构造方法对象的数组
        Constructor<?>[] constructors = aClass.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
        // Constructor<T> getConstructor(Class <?> ... parameterTypes) :返回单个公共构造方法对象
        // Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes) :返回单个构造方法对象
        // 参数:你要获取的构造方法的参数的个数和数据类型对应的字节码文件
        Constructor<?> constructor = aClass.getConstructor();
        // Constructor 提供了一个类的单个函数构造函数的信息和访问权限
        // T newInstance(Object... initargs) :根据指定的构造方法创建对象
        Object obj = constructor.newInstance();
        System.out.println(obj);
    }
}
练习:使用反射获取构造方法并调用

练习1:通过反射实现如下操作

  • Student s = new Student("张三", 20, "西安");
  • System.out.println(s);
  • 基本数据类型也可以通过.class得到对应的Class类型
public class ReflectDemo1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取 Class 对象
        Class<?> stuClass = Class.forName("get_class.Student");
        // Constructor<T> getConstructor(Class <?> ... parameterTypes) :返回单个公共构造方法对象
        // public Student(String name, int age, String address)
        Constructor<?> constructor = stuClass.getConstructor(String.class, int.class, String.class);
        // 基本数据类型也可以通过.class获取到 Class 类型
        Object obj = constructor.newInstance("张三", 20, "西安");
        System.out.println(obj);
    }
}

练习2:通过反射实现如下操作

  • Student s = new Student("张三");
  • System.out.println(s);
  • public void setAccessible(boolean flag)值为true,取消访问检查
public class ReflectDemo2 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取 Class 对象
        Class<?> stuClass = Class.forName("get_class.Student");
        // Constructor<T> getDeclaredConstructor(Class <?> ... parameterTypes) :返回单个构造方法对象
        // private Student(String name)
        Constructor<?> constructor = stuClass.getDeclaredConstructor(String.class);
        // java.lang.IllegalAccessException
        // 采用暴力反射,运行 setAccessible 方法,设置为 true 抑制访问检查
        constructor.setAccessible(true);
        Object obj = constructor.newInstance("张三");
        System.out.println(obj);
    }
}
反射获取成员变量并使用

Class类中用于获取成员变量的方法:

  • Field[] getFields():返回所有公共成员变量对象的数组、
  • Field[] getDeclaredFields():返回所有成员变量对象的数组
  • Field getField(String name):返回单个公共成员变量对象
  • Field getDeclaredField(String name):返回单个成员变量对象

Field类中用于给成员变量赋值的方法

  • void set(Object obj, Object value):给obj对象的成员变量赋值为value

案例:

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取对象的 Class 类型
        Class<?> aClass = Class.forName("get_class.Student");
        // Field[] getFields():返回所有公共成员变量对象的数组
        // Field[] fields = aClass.getFields();
        // Field[] getDeclaredFields():返回所有成员变量对象的数组
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }
        // Field getField(String name):返回单个公共成员变量对象
        // Field getDeclaredField(String name):返回单个成员变量对象
        Field addressField = aClass.getField("address");
        // 获取无参构造创建对象
        Constructor<?> constructor = aClass.getConstructor();
        Object obj = constructor.newInstance();
        // void set(Object obj, Object value):给obj对象的成员变量赋值为value
        addressField.set(obj, "西安");
        System.out.println(obj);
    }
}
练习:反射获取成员变量并使用

练习:通过反射实现如下操作

Student s = new Student();
s.name = "name" ;
s.age = 20;
s.address ="address";
System.out.println(s);

实现代码:

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        // 获得 Class 对象
        Class<?> stuClass = Class.forName("get_class.Student");
        // 获取无参构造方法创建对象
        Constructor<?> constructor = stuClass.getConstructor();
        Object obj = constructor.newInstance();
        System.out.println(obj);
        // 获取 field 对象、破坏访问权限检查、赋值、输出
        Field nameField = stuClass.getDeclaredField("name");
        nameField.setAccessible(true);
        nameField.set(obj, "雨下一整晚Real");
        System.out.println(obj);
        Field ageField = stuClass.getDeclaredField("age");
        ageField.setAccessible(true);
        ageField.set(obj, 20);
        System.out.println(obj);
        Field addressField = stuClass.getDeclaredField("address");
        addressField.setAccessible(true);
        addressField.set(obj, "Address");
        System.out.println(obj);
    }
}
反射获取成员方法并使用

Class类中用于获取成员方法的方法

  • Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
  • Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
  • Method getMethod(String name, Class<?> ... parameterTypes):返回单个公共成员方法对象
  • Method getDeclaredMethod(String name, Class <?> ... parameterTypes):返回单个成员方法对象

Method类中用于调用成员方法的方法

  • Object invoke(Object obj, Object... args):调用obj对象的成员方法,参数是args,返回值是Object类型

案例:

public class ReflectDemo {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 获取 Class 对象
        Class<?> stuClass = Class.forName("get_class.Student");
        // Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
        Method[] methods = stuClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
        // Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
        Method[] declaredMethods = stuClass.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println(declaredMethod);
        }
        // Method getMethod(String name, Class<?> ... parameterTypes):返回单个公共成员方法对象
        // Method getDeclaredMethod(String name, Class <?> ... parameterTypes):返回单个成员方法对象
        // public void method1()
        Method method = stuClass.getMethod("method1");

        // 获取无参构造方法创建对象
        Constructor<?> constructor = stuClass.getConstructor();
        Object obj = constructor.newInstance();
        // Object invoke(Object obj, Object... args):调用obj对象的成员方法,参数是args,返回值是Object类型
        method.invoke(obj);
    }
}
练习:反射获取成员方法并调用

练习:通过反射实现如下操作

Students = new Student);
s.method1();
s.method2("张三" );
String ss = s.method3("张三",30);
System.out.printIn(ss);
s.function();
public class ReflectDemo {
    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
        // 获取 Class 类型
        Class<?> stuClass = Class.forName("get_class.Student");
        // 获取无参构造方法创建对象
        Constructor<?> constructor = stuClass.getConstructor();
        Object obj = constructor.newInstance();
        // 通过对象调用 method1 方法
        Method method1 = stuClass.getMethod("method1");
        method1.invoke(obj);
        // 调用 method2 方法
        Method method2 = stuClass.getMethod("method2", String.class);
        method2.invoke(obj, "雨下一整晚Real");
        // 调用 method3 方法,存在返回值
        Method method3 = stuClass.getMethod("method3", String.class, int.class);
        Object o = method3.invoke(obj, "雨下一整晚Real", 20);
        System.out.println(o);
        // 调用 function 方法,私有方法
        // Method function = stuClass.getMethod("function"); java.lang.NoSuchMethodException
        Method function = stuClass.getDeclaredMethod("function");
        // 暴力反射,破坏方法调用前的权限检查
        function.setAccessible(true);
        function.invoke(obj);
    }
}
反射练习

练习1:有一个ArrayList<Integer>集合,现在要在这个集合中添加一个字符串数据,如何实现?

public class ArrayListAddString {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 创建集合
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
        /*arrayList.add(10);
        arrayList.add(20);
        arrayList.add("Hello");*/

        // 获取 Class 类型
        Class<? extends ArrayList> arrayClass = arrayList.getClass();
        Method add = arrayClass.getMethod("add", Object.class);
        // 利用反射越过泛型检查,调用原始方法的参数类型
        add.invoke(arrayList, "Hello");
        add.invoke(arrayList, "World");
        System.out.println(arrayList);
    }
}

练习2:通过配置文件运行类中的方法

public class ConfigurationFileRunMethodInClass {
    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
        /*
        * Class.txt
        * className=Xxx
        * methodName=Xxx
        * */
        // 加载配置文件
        Properties properties = new Properties();
        FileReader fr = new FileReader("D:\\Java\\IdeaProjects\\JavaBasic\\day16_Reflection\\src\\reflect_exercise\\Class.txt");
        properties.load(fr);
        fr.close();
        // className=reflect_exercise.Student
        // methodName=study
        String className = properties.getProperty("className");
        String methodName = properties.getProperty("methodName");
        // 通过调用反射来实现方法调用
        Class<?> stuClass = Class.forName(className);
        // 通过获取无参构造方法获取对象
        Constructor<?> constructor = stuClass.getConstructor();
        Object obj = constructor.newInstance();
        Method method = stuClass.getDeclaredMethod(methodName);
        method.invoke(obj);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,我无法回答关于2023年的问题,因为我无法提供未来的信息。但是,如果你对Java基础知识点有兴趣,我可以为你提供一些相关的信息。Java基础知识点包括数据类型、变量、运算符、控制流程、数组、字符串、面向对象编程等。你可以参考Java基础教程系列,其包含了Java基础知识点、Java8新特性、Java集合、Java多线程等内容,可以帮助你轻松学习Java编程。\[1\]另外,Javac是Java编译器程序的一部分,负责将Java源代码编译成字节码文件,也就是class文件,供Java虚拟机(JVM)执行。\[2\]Java分为三个体系,分别是Java SE(标准版)、Java EE(企业版)和Java ME(微型版),每个体系都有不同的用途和应用领域。\[3\]希望这些信息对你有帮助! #### 引用[.reference_title] - *1* [java基础知识点](https://blog.csdn.net/guorui_java/article/details/120317300)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Java基础知识点整理,推荐收藏!](https://blog.csdn.net/weixin_42599558/article/details/114148399)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值