java入门

Java 基础

IDEA 快捷键

快捷键 功能
Alt+Enter 导入包,自动修正代码
Ctrl+Y 删除光标所在行
Ctrl+D 复制光标所在行的内容,插入光标位置下面
Ctrl+Alt+L 格式化代码
Ctrl+/ 单行注释
Ctrl+Shift+/ 选中代码注释,多行注释,再按取消注释
Alt+Ins 自动生成代码,toString,get,set等方法(牢记)
Alt+Shift+上下箭头 移动当前代码行
Ctrl+Alt+v 根据右边值生成左边变量(牢记)

数据类型

数据类型分类

Java的数据类型分为两大类:

  • 基本数据类型:包括 整数浮点数字符布尔
  • 引用数据类型:包括 数组接口

基本数据类型

四类八种基本数据类型:

数据类型 关键字 内存占用 取值范围
字节型 byte 1个字节 -128~127
短整型 short 2个字节 -32768~32767
整型 int(默认) 4个字节 -231次方~2的31次方-1
长整型 long 8个字节 -2的63次方~2的63次方-1
单精度浮点数 float 4个字节 1.4013E-45~3.4028E+38
双精度浮点数 double(默认) 8个字节 4.9E-324~1.7977E+308
字符型 char 2个字节 0-65535
布尔类型 boolean 1个字节 true,false

Java中的默认类型:整数类型是 int 、浮点类型是 double
long类型:建议数据后加L表示。
float类型:建议数据后加F表示。

public class Variable {
    public static void main(String[] args){
        //定义字节型变量
        byte b = 100;
        System.out.println(b);
        //定义短整型变量
        short s = 1000;
        System.out.println(s);
        //定义整型变量
        int i = 123456;
        System.out.println(i);
        //定义长整型变量
        long l = 12345678900L;
        System.out.println(l);
        //定义单精度浮点型变量
        float f = 5.5F;
        System.out.println(f);
        //定义双精度浮点型变量
        double d = 8.5;
        System.out.println(d);
        //定义布尔型变量
        boolean bool = false;
        System.out.println(bool);
        //定义字符型变量
        char c = 'A';
        System.out.println(c);
    }
}

类型转换

不同范围的类型进行操作时, 会自动进行类型的提升

public static void main(String[] args) {
    int i = 1;
    double d = 2.5;
    //int类型和double类型运算,结果是double类型
    //int类型会提升为double类型
    double e = d+i;
    System.out.println(e);
}

范围小的类型向范围大的类型提升, byteshortchar 运算时直接提升为 int
byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double

循环

for, while, do while, 增强for

选择

if, switch case,
switch case不推荐, 如果忘写break, 后果很严重, 而且case后面的类型有限,(python只有if, 可以用dict)

一些常用类

Scanner类

Scanner 类来获取输入信息

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();
        System.out.println("s = " + s);
    }

Random类

Random生成随机数
public int nextInt(int n): 返回一个伪随机数, 范围在0(包括)和指定值n(不包括)之间的int值。

    public static void main(String[] args) {
        Random random = new Random();
        int i = random.nextInt();   
        int i1 = random.nextInt(5); 
        System.out.println("i = " + i);
        System.out.println("i1 = " + i1);
    }

ArrayList类

数组集合类, 连续的, 区别于LinkedList

String类

字符串类, 类String中包括用于检查各个字符串的方法,比如用于比较字符串,搜索字符串,提取子字符串以及创建具有翻译为大写或小写的所有字符的字符串的副本。

Arrays类

java.util.Arrays此类包含用来操作数组的各种方法,比如排序搜索等。其所有方法均为静态方法,调用起来非常简单。

Math类

java.lang.Math类包含用于执行基本数学运算的方法,如初等指数对数平方根三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。

Object类

java.lang.Object类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object

如果一个类没有特别指定父类, 那么默认则继承自Object类。例如:

public class MyClass /*extends Object*/ {
  	// ...
}

toString方法

返回该对象的字符串表示, 默认是对象的类型+@+内存地址值, python中的__str__

public class Person {  
    private String name;
    private int age;

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

    // 省略构造器与Getter Setter
}

equals方法

==比较的是2个对象的地址,而equals比较的是2个对象的内容
指示其他某个对象是否与此对象“相等”, 默认比较内存地址, python中的==

import java.util.Objects;

public class Person {	
	private String name;
	private int age;
	
    @Override
    public boolean equals(Object o) {
        // 如果对象地址一样,则认为相同
        if (this == o)
            return true;
        // 如果参数为空,或者类型信息不一样,则认为不同
        if (o == null || getClass() != o.getClass())
            return false;
        // 转换为当前类型
        Person person = (Person) o;
        // 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果
        return age == person.age && Objects.equals(name, person.name);
    }
}

Objects类

在刚才重写equals代码中,使用到了java.util.Objects类,那么这个类是什么呢?

在JDK7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。

在比较两个对象的时候,Objectequals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:

  • public static boolean equals(Object a, Object b): 判断两个对象是否相等。

源码:

public static boolean equals(Object a, Object b) {  
    return (a == b) || (a != null && a.equals(b));  
}

Date类

java.util.Date类 表示特定的瞬间,精确到毫秒。

import java.util.Date;

public class DateDemo {
    public static void main(String[] args) {
        System.out.println(new Date());
        System.out.println(new Date(0L));
        System.out.println(new Date().getTime());
    }
}


Sun Sep 29 11:31:36 CST 2019
Thu Jan 01 08:00:00 CST 1970
1569727896597

DateFormat类

java.text.DateFormat 是日期/时间格式化子类的抽象类,我们通过这个类可以帮我们完成日期和文本之间的转换,也就是可以在Date对象String对象之间进行来回转换。

  • 格式化:按照指定的格式,从Date对象转换为String对象
  • 解析:按照指定的格式,从String对象转换为Date对象

构造方法

由于DateFormat为抽象类,不能直接使用,所以需要常用的子类java.text.SimpleDateFormat。这个类需要一个模式(格式)来指定格式化或解析的标准。构造方法为:

  • public SimpleDateFormat(String pattern):用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat

参数pattern是一个字符串,代表日期时间的自定义格式。

标识字母(区分大小写) 含义
y
M
d
H
m
s

常用方法

  • public String format(Date date):将Date对象格式化为字符串。
  • public Date parse(String source):将字符串解析为Date对象。
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateDemo {
    public static void main(String[] args) throws ParseException {
        System.out.println(new Date());
        System.out.println(new Date(0L));
        System.out.println(new Date().getTime());

        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(format.format(new Date()));
        System.out.println(format.parse("2019-09-29 11:46:13"));
    }
}

Sun Sep 29 11:47:14 CST 2019
Thu Jan 01 08:00:00 CST 1970
1569728834328
2019-09-29 11:47:14
Sun Sep 29 11:46:13 CST 2019

System类

java.lang.System 类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有

  • public static long currentTimeMillis():返回以毫秒为单位的当前时间。
  • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中。

currentTimeMillis方法

currentTimeMillis方法就是 获取当前系统时间与1970年01月01日00:00点之间的毫秒差值

System.out.println(System.currentTimeMillis());

arraycopy方法

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length):将数组中指定的数据拷贝到另一个数组中。

数组的拷贝动作是系统级的,性能很高。System.arraycopy方法具有5个参数,含义分别为:

参数序号 参数名称 参数类型 参数含义
1 src Object 源数组
2 srcPos int 源数组索引起始位置
3 dest Object 目标数组
4 destPos int 目标数组索引起始位置
5 length int 复制元素个数
import java.util.Arrays;

public class Demo11SystemArrayCopy {
    public static void main(String[] args) {
        int[] src = new int[]{1,2,3,4,5};
        int[] dest = new int[]{6,7,8,9,10};
        System.arraycopy( src, 0, dest, 0, 3);
        /*代码运行后:两个数组中的元素发生了变化
         src数组元素[1,2,3,4,5]
         dest数组元素[1,2,3,9,10]
        */
    }
}

StringBuilder类

由于String类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象, 当进行多次拼接时, 会造成时间和空间上的浪费

在API中对String类有这样的描述:字符串是常量,它们的值在创建后不能被更改

StringBuilder被称为可变字符序列,它是一个类似于String的字符串缓冲区,是一个容器, 可以装很多字符串, 并且能够对其中的字符串进行各种操作。默认开辟16字符空间,超过自动扩充

构造方法

常用的两个

  • public StringBuilder():构造一个空的StringBuilder容器。
  • public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去。
    public StringBuilder() {
        super(16);
    }
    
    ...

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

常用方法

public StringBuilder append(...):添加任意类型数据的字符串形式,并返回当前对象自身。
public String toString():将当前StringBuilder对象转换为String对象。

包装类

Java提供了两个类型系统,基本类型引用类型,使用基本类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本类型对应的包装类,如下:

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

File类

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

构造方法

  • public File(String pathname) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。
  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。

常用方法

  • public String getAbsolutePath() :返回此File的绝对路径名字符串
  • public String getPath() :将此File转换为路径名字符串。
  • public String getName() :返回由此File表示的文件或目录的名称。
  • public long length() :返回由此File表示的文件的长度。
  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete() :删除由此File表示的文件或目录, 如果此File表示目录,则目录必须为空才能删除。
  • public boolean mkdir() :创建由此File表示的目录。
  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。
  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
public class FileDemo2 {
    public static void main(String[] args) {
        File dir = new File("e:\\java\\BaseStudy");

        System.out.println("list");
        //获取当前目录下的文件以及文件夹的名称。
        String[] names = dir.list();
        assert names != null;
        for (String name : names) {
            System.out.println(name);
        }

        System.out.println("listFiles");
        //获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
        File[] files = dir.listFiles();
        assert files != null;
        for (File file : files) {
            System.out.println(file);
        }
    }
}

list
.idea
BaseStudy.iml
out
src
listFiles
e:\java\BaseStudy\.idea
e:\java\BaseStudy\BaseStudy.iml
e:\java\BaseStudy\out
e:\java\BaseStudy\src

面向对象

访问修饰符(私有, 保护, 公开, 默认)

public protected default(空的) private
同一类中
同一包中(子类与无关类)
不同包的子类
不同包中的无关类

public具有最大权限。private则是最小权限。

编写代码时,如果没有特殊的考虑,建议这样使用权限:

  • 成员变量使用 private ,隐藏细节。
  • 构造方法使用 public ,方便创建对象。
  • 成员方法使用 public ,方便调用方法。

不加权限修饰符,其访问能力与default修饰符相同

static关键字

类变量

static修饰成员变量时,该变量称为类变量。该类的每个对象都共享同一个类变量的值。任何对象都可以更改该类变量的值,但也可以在不创建该类的对象的情况下对类变量进行操作。

类变量:使用 static关键字修饰的成员变量。
static 数据类型 变量名;

静态方法

static修饰成员方法时,该方法称为类方法 。静态方法在声明中有static,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。

类方法:使用 static关键字修饰的成员方法,习惯称为静态方法。

    修饰符 static 返回值类型 方法名 (参数列表){
    // 执行语句 
    }

静态方法调用的注意事项:
静态方法可以直接访问类变量和静态方法。
静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
静态方法中,不能使用this关键字。
静态方法只能访问静态成员。

调用格式

static修饰的成员可以并且建议通过类名直接访问。虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用同一个静态成员,但是不建议,会出现警告信息。

// 访问类变量
类名.类变量名;
// 调用静态方法
类名.静态方法名(参数);

静态原理图解

static 修饰的内容:

  • 是随着类的加载而加载的,且只加载一次。
  • 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
  • 它优先于对象存在,所以,可以被所有对象共享。

静态原理图解

静态代码块

静态代码块:定义在成员位置,使用static修饰的代码块{ }。

  • 位置:类中方法外。
  • 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
   public class ClassName{
       static {
       // 执行语句
       }
   }

作用:给类变量进行初始化赋值。用法演示,代码如下:

public class Game {
    public static int number;
    public static ArrayList<String> list;
    
    static {
        // 给类变量赋值
        number = 2;
        list = new ArrayList<String>();
            // 添加元素到集合中
            list.add("张三");
            list.add("李四");
    }
}

小贴士:
static 关键字,可以修饰变量、方法和代码块。在使用的过程中,其主要目的还是想在不创建对象的情况下,去调用方法。

super和this

父类空间优先于子类对象产生

在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。

super和this的含义

  • super :代表父类的存储空间标识(可以理解为父亲的引用)。
  • this :代表当前对象的引用(谁调用就代表谁)。

子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。
super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

继承

封装, 继承, 多态是面向对象的三大特征, Java仅支持单继承, 但可实现多接口

使用继承后, 子类中如果想使用父类中的同名变量时, 需要使用super关键字; 子类使用自身的变量则不需要super关键字

class Fu{
    int age=35;
    String name="fu";

}

class Zi extends Fu{
    int age=10;
    String name = "zi";

    void show(){
        System.out.println("Zi.age = " + age);
        System.out.println("Zi.name = " + name);
        
        System.out.println("Fu.age = " + super.age);
        System.out.println("Fu.name = " + super.name);
    }
}
    
public class Extend {

    public static void main(String[] args) {
        Zi zi = new Zi();
        zi.show();
    }
}


Zi.age = 10
Zi.name = zi
Fu.age = 35
Fu.name = fu

仅可访问非私有父类成员变量, 若想访问父类的私有成员变量, 则需要父类提供getset方法(封装原则)

子类继承父类后, 若要重写父类的方法, 则需要保证权限大于等于父类权限
返回值类型、函数名和参数列表都要一模一样。

多态

同一行为, 具有多个不同表现形式.

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】
父类类型 变量名 = new 子类对象;
变量名.方法名();

// 父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
Fu f = new Zi();
f.method();

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。

定义父类:

public abstract class Animal {  
    public abstract void eat();  
}  

定义子类:

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

定义测试类:

public class Test {
    public static void main(String[] args) {
        // 多态形式,创建对象
        Animal a1 = new Cat();  
        // 调用的是 Cat 的 eat
        a1.eat();          
 
        // 多态形式,创建对象
        Animal a2 = new Dog(); 
        // 调用的是 Dog 的 eat
        a2.eat();               
    }  
}

多态的好处

实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。

定义父类:

public abstract class Animal {  
    public abstract void eat();  
}

定义子类:

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

定义测试类:

public class Test {
    public static void main(String[] args) {
        // 多态形式,创建对象
        Cat c = new Cat();  
        Dog d = new Dog(); 
 
        // 调用showCatEat 
        showCatEat(c);
        // 调用showDogEat 
        showDogEat(d); 
 
        /*
        以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
        而执行效果一致
        */
        showAnimalEat(c);
        showAnimalEat(d); 
    }
 
    public static void showCatEat (Cat c){
        c.eat(); 
    }
 
    public static void showDogEat (Dog d){
        d.eat();
    }
 
    public static void showAnimalEat (Animal a){
        a.eat();
    }
}

引用类型转换

多态的转型分为向上转型与向下转型两种:

向上转型

向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。

当父类引用指向一个子类对象时,便是向上转型。

父类类型  变量名 = new 子类类型();
如:Animal a = new Cat();

向下转型

向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;  

为什么要转型

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。

定义类:

abstract class Animal {  
    abstract void eat();  
}  
 
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void catchMouse() {  
        System.out.println("抓老鼠");  
    }  
}  
 
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void watchHouse() {  
        System.out.println("看家");  
    }  
}

定义测试类:

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();                // 调用的是 Cat 的 eat
 
        // 向下转型  
        Cat c = (Cat)a;       
        c.catchMouse();         // 调用的是 Cat 的 catchMouse
    }  
}

转型的异常

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat
 
        // 向下转型  
        Dog d = (Dog)a;       
        d.watchHouse();        // 调用的是 Dog 的 watchHouse 【运行报错】
    }  
}

这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验

变量名 instanceof 数据类型 
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat
 
        // 向下转型  
        if (a instanceof Cat){
            Cat c = (Cat)a;       
            c.catchMouse();        // 调用的是 Cat 的 catchMouse
        } else if (a instanceof Dog){
            Dog d = (Dog)a;       
            d.watchHouse();       // 调用的是 Dog 的 watchHouse
        }
    }  
}

接口

接口,是Java语言中一种引用类型,是方法的集合,如果说类的内部封装了成员变量、构造方法和成员方法,那么接口的内部主要就是封装了方法,包含抽象方法(JDK7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9)。

接口的定义,它与定义类方式相似,但是使用interface关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组接口

接口的使用,它不能创建对象,但是可以被实现(implements类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。

格式

public interface 接口名称 {
    // 抽象方法
    // 默认方法
    // 静态方法
    // 私有方法
}

抽象方法:使用 abstract关键字修饰,可以省略,没有方法体。该方法供子类实现使用。

public interface InterFaceName {
    public abstract void method();
}

默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。
静态方法:使用 static 修饰,供接口直接调用。

public interface InterFaceName {
    public default void method() {
        // 执行语句
    }
    public static void method2() {
        // 执行语句    
    }
}

私有方法:使用 private 修饰,供接口中的默认方法或者静态方法调用。

public interface InterFaceName {
    private void method() {
        // 执行语句
    }
}

基本实现

非抽象子类实现接口:

  1. 必须重写接口中所有抽象方法.
  2. 继承了接口的默认方法, 即可以直接调用, 也可以重写.

实现格式:

class 类名 implements 接口名 {
    // 重写接口中抽象方法【必须】
    // 重写接口中默认方法【可选】
} 

抽象方法的使用: 必须全部实现
默认方法的使用: 可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。
静态方法的使用: 静态与.class文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用
私有方法的使用 : 私有方法:只有默认方法可以调用。私有静态方法:默认方法和静态方法可以调用。

静态方法的使用

public interface LiveAble {
    public static void run(){
        System.out.println("跑起来~~~");
    }
}

public class Animal implements LiveAble {
    // 无法重写静态方法
}

public class InterfaceDemo {
    public static void main(String[] args) {
        // Animal.run(); // 【错误】无法继承方法,也无法调用
        LiveAble.run(); // 
    }
}

跑起来~~~

接口的多实现

一个类可以实现多个接口

class 类名 [extends 父类名] implements 接口名1,接口名2,接口名3... {
    // 重写接口中抽象方法【必须】
    // 重写接口中默认方法【不重名时可选】
} 

抽象方法

接口中,有多个抽象方法时,实现类必须重写所有抽象方法如果抽象方法有重名的,只需要重写一次。

interface A {
    public abstract void showA();
    public abstract void show();
}
 
interface B {
    public abstract void showB();
    public abstract void show();
}
public class C implements A,B{
    @Override
    public void showA() {
        System.out.println("showA");
    }
 
    @Override
    public void showB() {
        System.out.println("showB");
    }
 
    @Override
    public void show() {
        System.out.println("show");
    }
}

默认方法

接口中,有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次。

interface A {
    public default void methodA(){}
    public default void method(){}
}
 
interface B {
    public default void methodB(){}
    public default void method(){}
}
public class C implements A,B{
    @Override
    public void method() {
        System.out.println("method");
    }
}

静态方法

接口中,存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。

优先级的问题

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法。

定义接口:

interface A {
    public default void methodA(){
        System.out.println("AAAAAAAAAAAA");
    }
}

定义父类:

class D {
    public void methodA(){
        System.out.println("DDDDDDDDDDDD");
    }
}

定义子类:

class C extends D implements A {
    // 未重写methodA方法
}

定义测试类:

public class Test {
    public static void main(String[] args) {
        C c = new C();
        c.methodA(); 
    }
}


DDDDDDDDDDDD

接口的多继承

一个接口能继承另一个或者多个接口,这和类之间的继承比较相似。接口的继承使用 extends 关键字,子接口继承父接口的方法。如果父接口中的默认方法有重名的,那么子接口需要重写一次。

定义父接口:

interface A {
    public default void method(){
        System.out.println("AAAAAAAAAAAAAAAAAAA");
    }
}
 
interface B {
    public default void method(){
        System.out.println("BBBBBBBBBBBBBBBBBBB");
    }
}

定义子接口:

interface D extends A,B{
    @Override
    public default void method() {
        System.out.println("DDDDDDDDDDDDDD");
    }
}

子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。

小结

  • 接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final修饰。
  • 接口中,没有构造方法,不能创建对象。
  • 接口中,没有静态代码块

抽象类

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

修饰符 abstract 返回值类型 方法名 (参数列表);

public abstract void run();

如果一个类中含有抽象方法, 那么该类必须是抽象类

public abstract class Animal {
    public abstract void run();
}

使用

继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。

注意事项

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

final

用于修饰不可改变内容。
final: 不可改变。可以用于修饰类、方法和变量。

  • :被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,不能被重新赋值。

修饰类

final class 类名 {
  
}

public final class Stringpublic final class Mathpublic final class Scanner等,都是被final修饰的,目的就是供我们使用,而不让我们随意改变其内容。

修饰方法

修饰符 final 返回值类型 方法名(参数列表){
    //方法体
}

重写被 final 修饰的方法,编译时就会报错。

修饰变量

局部变量 - 基本类型

基本类型的局部变量,被final修饰后,只能赋值一次,不能再更改。

public class FinalDemo1 {
    public static void main(String[] args) {
        // 声明变量,使用final修饰
        final int a;
        // 第一次赋值 
        a = 10;
        // 第二次赋值
        a = 20; // 报错,不可重新赋值
 
 
        // 声明变量,直接赋值,使用final修饰
        final int b = 10;
        // 第二次赋值
        b = 20; // 报错,不可重新赋值
    }
}

局部变量 - 引用类型

引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。但是不影响对象内部的成员变量值的修改

public class FinalDemo2 {
    public static void main(String[] args) {
        // 创建 User 对象
        final   User u = new User();
        // 创建 另一个 User对象
        u = new User(); // 报错,指向了新的对象,地址值改变。
 
        // 调用setName方法
        u.setName("张三"); // 可以修改
    }
}

成员变量

成员变量涉及到初始化的问题,初始化方式有两种,只能二选一:

  • 显示初始化
public class User {
    final String USERNAME = "张三";
    private int age;
}
  • 构造方法初始化
public class User {
    final String USERNAME ;
    private int age;
    public User(String username, int age) {
        this.USERNAME = username;
        this.age = age;
    }
}

被final修饰的常量名称,一般都有书写规范,所有字母都大写。

内部类

class 外部类 {
    class 内部类{
 
    }
}

访问特点

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();

定义类:

public class Person {
    private  boolean live = true;
    class Heart {
        public void jump() {
            // 直接访问外部类成员
            if (live) {
                System.out.println("心脏在跳动");
            } else {
                System.out.println("心脏不跳了");
            }
        }
    }
 
    public boolean isLive() {
        return live;
    }
 
    public void setLive(boolean live) {
        this.live = live;
    }
 
}

定义测试类:

public class InnerDemo {
    public static void main(String[] args) {
        // 创建外部类对象 
        Person p  = new Person();
        // 创建内部类对象
        Person.Heart heart = p.new Heart();
 
        // 调用内部类方法
        heart.jump();
        // 调用外部类方法
        p.setLive(false);
        // 调用内部类方法
        heart.jump();
    }
}


心脏在跳动
心脏不跳了

内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号 。
比如,Person$Heart.class

匿名内部类

匿名内部类 :是内部类的简化写法。它的本质是一个带具体实现的 父类或者父接口的 匿名的 子类对象

匿名内部类必须继承一个父类或者实现一个父接口

new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

定义接口:

public abstract class FlyAble{
    public abstract void fly();
}

创建匿名内部类,并调用:

public class InnerDemo {
    public static void main(String[] args) {
        /*
        1.等号右边:是匿名内部类,定义并创建该接口的子类对象
        2.等号左边:是多态赋值,接口类型引用指向子类对象
        */
        FlyAble  f = new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
 
        //调用 fly方法,执行重写后的方法
        f.fly();
    }
}

通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递

public class InnerDemo2 {
    public static void main(String[] args) {
        /*
        1.等号右边:定义并创建该接口的子类对象
        2.等号左边:是多态,接口类型引用指向子类对象
       */
        FlyAble  f = new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        };
        // 将f传递给showFly方法中
        showFly(f);
    }
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

以上两步,也可以简化为一步

public class InnerDemo3 {
    public static void main(String[] args) {          
        /*
        创建匿名内部类,直接传递给showFly(FlyAble f)
        */
        showFly( new FlyAble(){
            public void fly() {
                System.out.println("我飞了~~~");
            }
        });
    }
 
    public static void showFly(FlyAble f) {
        f.fly();
    }
}

引用类型用法

基本类型可以作为成员变量、作为方法的参数、作为方法的返回值,那么当然引用类型也是可以的。

class作为成员变量

interface作为成员变量

interface作为方法参数和返回值类型

接口作为参数时,传递它的子类对象。
接口作为返回值类型时,返回它的子类对象。

native

Java调用非Java代码的接口, 方法由非Java语言实现

集合

集合是java中提供的一种容器,可以用来存储多个数据. 集合的长度是可变的, 可以存储不同的数据类型

单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.Listjava.util.Set。其中,List的特点是元素有序元素可重复Set的特点是元素无序,而且不可重复List接口的主要实现类有java.util.ArrayListjava.util.LinkedListSet接口的主要实现类有java.util.HashSetjava.util.TreeSet

Collection 常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(ListSet)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e):把给定的对象添加到当前集合中 。
  • public void clear(): 清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

Iterator迭代器

JDK提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历Collection中的元素,因此Iterator对象也被称为迭代器

想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作.

  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。
  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

Iterator接口的常用方法:

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回true
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        // 使用多态方式 创建对象
        Collection<String> coll = new ArrayList<String>();

        // 添加元素到集合
        coll.add("123");
        coll.add("456");
        coll.add("789");
        //遍历
        //使用迭代器 遍历   每个集合对象都有自己的迭代器
        Iterator<String> it = coll.iterator();
        //  泛型指的是 迭代出 元素的数据类型
        while(it.hasNext()){ //判断是否有迭代元素
            String s = it.next();//获取迭代出的元素
            System.out.println(s);
        }
    }
}

123
456
789

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException没有集合元素的错误。

java/util/ArrayList.java

/**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

增强for

增强for是一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。

for(元素的数据类型  变量 : Collection集合or数组){ 
  	//写操作代码
}

泛型

泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

public interface Collection<E> extends Iterable<E>{
    
    ...

    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean containsAll(Collection<?> c);
    ...

}

通配符高级使用----受限泛型

设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

  • 泛型的上限
    格式: 类型名称 <? extends 类 > 对象名称
    意义: 只能接收该类型及其子类

  • 泛型的下限
    格式: 类型名称 <? super 类 > 对象名称
    意义: 只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

    public static void main(String[] args) {
        Collection<Integer> list1 = new ArrayList<Integer>();
        Collection<String> list2 = new ArrayList<String>();
        Collection<Number> list3 = new ArrayList<Number>();
        Collection<Object> list4 = new ArrayList<Object>();
        
        getElement(list1);
        getElement(list2);//报错
        getElement(list3);
        getElement(list4);//报错
      
        getElement2(list1);//报错
        getElement2(list2);//报错
        getElement2(list3);
        getElement2(list4);
      
    }
    // 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
    public static void getElement1(Collection<? extends Number> coll){}
    // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
    public static void getElement2(Collection<? super Number> coll){}

List集合

List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法:

  • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
  • public E get(int index) :返回集合中指定位置的元素。
  • public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
  • public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

ArrayList

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢查找快

LinkedList

java.util.LinkedList 集合数据存储的结构是链表结构, 是一个双向链表。方便元素添加删除的集合。

对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。

  • public void addFirst(E e) : 将指定元素插入此列表的开头。
  • public void addLast(E e) : 将指定元素添加到此列表的结尾。
  • public E getFirst() : 返回此列表的第一个元素。
  • public E getLast() : 返回此列表的最后一个元素。
  • public E removeFirst() : 移除并返回此列表的第一个元素。
  • public E removeLast() : 移除并返回此列表的最后一个元素。
  • public E pop() : 从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e) : 将元素推入此列表所表示的堆栈。
  • public boolean isEmpty() :如果列表不包含元素,则返回true。

Set接口

java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。

HashSet

java.util.HashSetSet 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个java.util.HashMap 支持

HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取查找性能。保证元素唯一性的方式依赖于: hashCodeequals 方法。

HashSet存储自定义类型元素

HashSet中存放自定义类型元素时,需要重写对象中的hashCodeequals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一

IDEA自动生成的方法

import java.util.Objects;

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

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public 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 &&
                name.equals(student.name);
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}                                       
import java.util.HashSet;

public class HashSetDemo {
    public static void main(String[] args) {
        HashSet<Student> hashSet = new HashSet<>();

        hashSet.add(new Student("张三", 20));
        hashSet.add(new Student("李四", 18));
        hashSet.add(new Student("王五", 22));
        hashSet.add(new Student("赵六", 25));

        for (Student s : hashSet) {
            System.out.println(s);
        }
    }
}

Student{name='张三', age=20}
Student{name='王五', age=22}
Student{name='李四', age=18}
Student{name='赵六', age=25}

LinkedHashSet

java.util.LinkedHashSet, 它是链表和哈希表组合的一个有序数据存储结构。

import java.util.HashSet;
import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        HashSet<String> hashSet = new HashSet<>();

        linkedHashSet.add("张三");
        linkedHashSet.add("李四");
        linkedHashSet.add("王五");
        linkedHashSet.add("赵六");

        hashSet.add("张三");
        hashSet.add("李四");
        hashSet.add("王五");
        hashSet.add("赵六");

        System.out.println("hashSet = " + hashSet.toString());
        System.out.println("linkedHashSet = " + linkedHashSet.toString());
    }
}

hashSet = [李四, 张三, 王五, 赵六]
linkedHashSet = [张三, 李四, 王五, 赵六]

Collections

java.utils.Collections 是集合工具类,用来对集合进行操作。

  • public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。
  • public static void shuffle(List<?> list) :打乱集合顺序。
  • public static <T> void sort(List<T> list) :将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ) :将集合中元素按照自定义的规则排序。
import java.util.ArrayList;
import java.util.Collections;

public class CollectionsDemo {
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();

        // arrayList.add("123");
        // arrayList.add("789");
        // arrayList.add("654");
        // arrayList.add("369");

        Collections.addAll(arrayList, "123", "789", "654", "369");

        System.out.println("arrayList: " + arrayList.toString());

        Collections.sort(arrayList);

        System.out.println("arrayList: " + arrayList.toString());
    }
}

Comparator比较器

public static <T> void sort(List<T> list) : 将集合中元素按照默认规则排序。

public int compare(String o1, String o2) : 比较其两个参数的顺序。

两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序, 则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数) 如果要按照降序排序 则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

Comparable和Comparator两个接口的区别

  • Comparable强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。需重写compareTo方法
  • Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sortArrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。需重写compare方法

Comparable

import java.util.Objects;

public class Student implements Comparable<Student>{
    private String name;
    private int age;

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

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

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

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

    @Override
    public int compareTo(Student o) {
        int res = name.compareTo(o.name);
        if (0==res) return age - o.age;
        else return res;
    }
}
import java.util.ArrayList;
import java.util.Collections;

public class ComparableDemo {
    public static void main(String[] args) {
        ArrayList<Student> arrayList = new ArrayList<Student>();

        arrayList.add(new Student("123", 22));
        arrayList.add(new Student("567", 17));
        arrayList.add(new Student("567", 15));
        arrayList.add(new Student("346", 20));

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

        Collections.sort(arrayList);

        System.out.println(arrayList.toString());
    }
}

[Student{name='123', age=22}, Student{name='567', age=17}, Student{name='567', age=15}, Student{name='346', age=20}]
[Student{name='123', age=22}, Student{name='346', age=20}, Student{name='567', age=15}, Student{name='567', age=17}]

Comparator

import java.util.Objects;

public class Student /*implements Comparable<Student>Student*/{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

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

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

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

//    @Override
//    public int compareTo(Student o) {
//        int res = name.compareTo(o.name);
//        if (0==res) return age - o.age;
//        else return res;
//    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

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

        ArrayList<Student> arrayList = new ArrayList<Student>();

        arrayList.add(new Student("123", 22));
        arrayList.add(new Student("567", 17));
        arrayList.add(new Student("567", 15));
        arrayList.add(new Student("346", 20));

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

        Collections.sort(arrayList, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                int res = o1.getAge() - o2.getAge();
                if (res == 0)return o1.getName().compareTo(o2.getName());
                else return res;
            }
        });

        System.out.println(arrayList.toString());
    }
}

[Student{name='123', age=22}, Student{name='567', age=17}, Student{name='567', age=15}, Student{name='346', age=20}]
[Student{name='567', age=15}, Student{name='567', age=17}, Student{name='346', age=20}, Student{name='123', age=22}]

map

  • HashMap:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
  • LinkedHashMapHashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证键的唯一不重复,需要重写键的hashCode()方法、equals()方法。

Map接口中的集合都有两个泛型变量,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量的数据类型可以相同,也可以不同。

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class MapDemo {

    private void addElement(Map<String, String> map){
        map.put("张三", "aaa");
        map.put("李四", "bbb");
        map.put("王五", "ccc");
        map.put("赵六", "ddd");
    }

    private void showHashMap(){
        HashMap<String, String> stringHashMap = new HashMap<>();
        addElement(stringHashMap);

        System.out.println(stringHashMap.toString());
    }

    private void showLinkedHashMap(){
        LinkedHashMap<String, String> stringLinkedHashMap = new LinkedHashMap<>();
        addElement(stringLinkedHashMap);

        System.out.println(stringLinkedHashMap.toString());
    }


    public static void main(String[] args) {
        new MapDemo().showHashMap();
        new MapDemo().showLinkedHashMap();
    }
}

{李四=bbb, 张三=aaa, 王五=ccc, 赵六=ddd}
{张三=aaa, 李四=bbb, 王五=ccc, 赵六=ddd}

常用方法

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
  • public V get(Object key): 根据指定的键, 在Map集合中获取对应的值, 不存在则返回null
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

Entry键值对对象

Map中的一个键值对对象就是一个Entry

  • public K getKey() :获取Entry对象中的键。
  • public V getValue() :获取Entry对象中的值。
//        Set<Map.Entry<String, String>> entries = stringHashMap.entrySet();
//
//        for (Map.Entry<String, String> entry : entries){
//            System.out.print(entry.getKey());
//            System.out.println(entry.getValue());
//        }

        for (Map.Entry<String, String> entry : stringHashMap.entrySet()) {
            System.out.println("entry = " + entry);
        }

Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。

HashMap存储自定义类型键值

  • 给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCodeequals方法
  • 如果要保证map中存放的key和取出的顺序一致,可以使用 java.util.LinkedHashMap 集合来存放

异常

程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

异常机制其实是帮助我们找到程序中的问题,异常的根类是 java.lang.Throwable ,其下有两个子类:
java.lang.Errorjava.lang.Exception, 平常所说的异常指java.lang.Exception

Throwable体系:

  • Error: 严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception: 表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

Throwable中的常用方法:

  • public void printStackTrace() :打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace
  • public String getMessage() :获取发生异常的原因。提示给用户的时候,就提示错误原因。
  • public String toString() :获取异常的类型和异常描述信息(不用)。

异常的处理

throw

throw new NullPointerException("要访问的arr数组不存在");
 
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

Objects非空判断

java/util/Objects.java:202

    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

声明异常throws

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }
public class ThrowsDemo {
    public static void main(String[] args) throws FileNotFoundException {
        read("a.txt");
    }
 
    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

捕获异常try…catch

try{
     编写可能会出现异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}
public class TryCatchDemo {
    public static void main(String[] args) {
        try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
            read("b.txt");
        } catch (FileNotFoundException e) {// 括号中需要定义什么呢?
            //try中抛出的是什么异常,在括号中就定义什么异常类型
            System.out.println(e);
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

finally代码块

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

try...catch....finally:自身需要处理异常,最终还得关闭资源。
public class TryCatch{
    public static void main(String[] args) {
        try {
            read("a.txt");
        } catch (FileNotFoundException e) {
            //抓取到的是编译期异常  抛出去的是运行期 
            throw new RuntimeException(e);
        } finally {
            System.out.println("不管程序怎样,这里都将会被执行。");
        }
        System.out.println("over");
    }
    /*
     *
     * 我们 当前的这个方法中 有异常  有编译期异常
     */
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

自定义异常

1. 自定义一个编译期异常: 自定义类 并继承于 java.lang.Exception 。
2. 自定义一个运行时期的异常类:自定义类 并继承于 java.lang.RuntimeException 。

线程和进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程调度:

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

多线程

Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
通过调用start()方法来通知JVM来开辟新的线程空间, 并在新的线程空间里面执行对象的run方法
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

Thread类

构造方法

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式

Runnable接口

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}

Runnable对象仅仅作为Thread对象的targetRunnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其targetrun()方法。

Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

匿名内部类方式实现线程的创建

public class NoNameInnerClassThread {
    public static void main(String[] args) {        
//      new Runnable(){
//          public void run(){
//              for (int i = 0; i < 20; i++) {
//                  System.out.println("张宇:"+i);
//              }
//          }  
//      }; //‐‐‐这个整体  相当于new MyRunnable()
        Runnable r = new Runnable(){
            public void run(){
                for (int i = 0; i < 20; i++) {
                    System.out.println("张宇:"+i);
                }
            }  
        };

        new Thread(r).start();
 
        for (int i = 0; i < 20; i++) {
            System.out.println("费玉清:"+i);
        }
    }
}

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
多线程如果不加约束, 很容易出问题的.

线程安全问题都是由全局变量静态变量引起的。若每个线程中对全局变量、静态变量只有操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

要解决多线程并发访问一个资源的安全性问题,Java中提供了同步机制(synchronized)来解决。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
(BLOCKED)。

public class Ticket implements Runnable{
    private int ticket = 100;
    
    Object lock = new Object();
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            synchronized (lock) {
                if(ticket>0){//有票 可以卖
                    //出票操作
                    //使用sleep模拟一下出票时间 
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // TODO Auto‐generated catch block
                        e.printStackTrace();
                    }
                    //获取当前线程对象的名字 
                    String name = Thread.currentThread().getName();
                    System.out.println(name+"正在卖:"+ticket‐‐);
                }
            }
        }
    }
}

同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

public synchronized void method(){
    可能会产生线程安全问题的代码
}

同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

public class Ticket implements Runnable{
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            sellTicket();
        }
    }
    
    /*
     * 锁对象 是 谁调用这个方法 就是谁 
     *   隐含 锁对象 就是  this
     *    
     */
    public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖   
            //出票操作
            //使用sleep模拟一下出票时间 
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto‐generated catch block
                e.printStackTrace();
            }
            //获取当前线程对象的名字 
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
    }
}

锁机制(Lock锁)

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

public void lock(): 加同步锁。
public void unlock(): 释放同步锁。

public class Ticket implements Runnable{
    private int ticket = 100;
    
    Lock lock = new ReentrantLock();
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while(true){
            lock.lock();
            if(ticket>0){//有票 可以卖
                //出票操作 
                //使用sleep模拟一下出票时间 
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //获取当前线程对象的名字 
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖:"+ticket‐‐);
            }
            lock.unlock();
        }
    }
}

线程状态

API中 java.lang.Thread.State 这个枚举中给出了六种线程状态

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡, 或者因为没有捕获的异常终止了run方法而死亡。

Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

当调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

 public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print("    线程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new MyThread().start();
    }
}
  1. 进入TIMED_WAITING状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态

sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。

BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态

Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

public class WaitingTest {
    public static Object obj = new Object();
 
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait();  //无限等待
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
 
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
//                while (true){   //每隔3秒 唤醒一次
 
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
 
                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
                        obj.notify();
                    }
                }
//            }
        },"唤醒线程").start();
    }
}

一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify() 方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

状态转换

翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的, 比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是 如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两 得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒 计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

等待唤醒机制

线程间通信

最常见的生产者消费者模型, 一个线程生产, 一个线程消费

为什么要处理线程间通信
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制

什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify就是线程间的一种协作机制。

等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是通知notify在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

线程池

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
 
        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ‐‐‐> 调用MyRunnable中的run()
 
        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        service.shutdown();
    }
}

Lambda表达式

() ‐> System.out.println("多线程任务执行!")
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println("多线程任务执行!");
    }
}

Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数, 一个箭头, 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) ‐> { 代码语句 }

小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。

排序时使用lambda表达式

 Arrays.sort(array, (Person a, Person b) ‐> {
            return a.getAge() ‐ b.getAge();
        });

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的 Runnable 、 Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

有且仅有一个抽象方法的接口,称为“函数式接口”。

多进程

IO

IO的分类

根据数据的流向分为:输入流和输出流。

  • 输入流 :把数据从 其他设备 上读取到 内存 中的流。
  • 输出流 :把数据从 内存 中写出到 其他设备 上的流。

格局数据的类型分为:字节流和字符流。

  • 字节流 :以字节为单位,读写数据的流。
  • 字符流 :以字符为单位,读写数据的流。
顶级父类们 输入流 输出流
字节流 字节输入流 InputStream 字节输出流 OutputStream
字符流 字符输入流 Reader 字符输出流 Writer

字节流

字节输出流【OutputStream】

java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileOutputStream类

java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。

  • public FileOutputStream(File file) :创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name) : 创建文件输出流以指定的名称写入文件。
  • public FileOutputStream(File file, boolean append) : 创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name, boolean append) : 创建文件输出流以指定的名称写入文件
public class FileOutputStreamConstructor throws IOException {
    public static void main(String[] args) {
        // 使用File对象创建流对象
        File file = new File("a.txt");
        FileOutputStream fos = new FileOutputStream(file);
      
        // 使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("b.txt");
    }
}

字节输入流【InputStream】

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read() : 从输入流读取数据的下一个字节。
  • public int read(byte[] b) : 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileInputStream类

java.io.FileInputStream 类是文件输入流,从文件中读取字节。

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

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出 FileNotFoundException

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class IOdemo1 {

    private void writeToFile() throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("Iodemo1.txt");
        byte[] val = "IO测试文件1".getBytes();

        fileOutputStream.write(val);
        fileOutputStream.close();
    }

    private void readFromFile() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("Iodemo1.txt");
        // 读取一个字节
        int i = fileInputStream.read();
        System.out.println("i = " + (char)i);

        // 在读取一个字节
        int o = fileInputStream.read();
        System.out.println("o = " + o);
        System.out.println("(char)o = " + (char)o);

        // 读取12个字节
        byte [] tmp = new byte [12];
        int read = fileInputStream.read(tmp);
        System.out.println("read = " + read);
        System.out.println(new String(tmp));

        fileInputStream.close();
    }

    public static void main(String[] args) throws IOException {
        IOdemo1 iOdemo1 = new IOdemo1();
        iOdemo1.writeToFile();
        iOdemo1.readFromFile();

    }
}

i = I
o = 79
(char)o = O
read = 12
测试文件

文件复制

public class Copy {
    public static void main(String[] args) throws IOException {
        // 1.创建流对象
        // 1.1 指定数据源
        FileInputStream fis = new FileInputStream("D:\\test.jpg");
        // 1.2 指定目的地
        FileOutputStream fos = new FileOutputStream("test_copy.jpg");
 
        // 2.读写数据
        // 2.1 定义数组
        byte[] b = new byte[1024];
        // 2.2 定义长度
        int len;
        // 2.3 循环读取
        while ((len = fis.read(b))!=‐1) {
            // 2.4 写出数据
            fos.write(b, 0 , len);
        }
 
        // 3.关闭资源
        fos.close();
        fis.close();
    }
}

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。

字符输入流【Reader】

java.io.Reader 抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close():关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

FileReader类

java.io.FileReader类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileReader(File file):创建一个新的 FileReader,给定要读取的File对象。
  • FileReader(String fileName):创建一个新的 FileReader,给定要读取的文件的名称.
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class IOdemo2 {
    private void writeToFile() throws IOException {
        FileWriter fileWriter = new FileWriter("Iodemo2.txt");
        fileWriter.write("Io测试文件2");

        fileWriter.close();
    }

    private void readFromFile() throws IOException {
        FileReader fileReader = new FileReader("Iodemo2.txt");

        int value;
        while((value=fileReader.read())!=-1){
            System.out.println("(char)value = " + (char)value);
        }

        fileReader.close();
    }

    public static void main(String[] args) throws IOException {
        IOdemo2 iOdemo2 = new IOdemo2();
        iOdemo2.writeToFile();
        iOdemo2.readFromFile();

    }
}

(char)value = I
(char)value = o
(char)value = 测
(char)value = 试
(char)value = 文
(char)value = 件
(char)value = 2

字符输出流【Writer】

  • java.io.Writer : 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
  • void write(int c) 写入单个字符。
  • void write(char[] cbuf) 写入字符数组。
  • abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分,off数组的开始索引,len写的字符个数。
  • void write(String str) 写入字符串。
  • void write(String str, int off, int len) 写入字符串的某一部分, off字符串的开始索引, len写的字符个数。
  • void flush() 刷新该流的缓冲。
  • void close() 关闭此流,但要先刷新它。

FileWriter类

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区

  • FileWriter(File file) : 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName) : 创建一个新的 FileWriter,给定要读取的文件的名称。

缓冲流

缓冲流,也叫高效流,是对4个基本的FileXxx流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流: BufferedInputStreamBufferedOutputStream
  • 字符缓冲流: BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

字节缓冲流

  • public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out) : 创建一个新的缓冲输出流。
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

字符缓冲流

  • public BufferedReader(Reader in) :创建一个新的缓冲输入流。
  • public BufferedWriter(Writer out) : 创建一个新的缓冲输出流。
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

特有方法

  • BufferedReaderpublic String readLine() : 读一行文字。
  • BufferedWriterpublic void newLine() : 写一行行分隔符,由系统属性定义符号。
// public String readLine

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {
         // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("in.txt"));
        // 定义字符串,保存读取的一行文字
        String line  = null;
        // 循环读取,读取到最后返回null
        while ((line = br.readLine())!=null) {
            System.out.print(line);
            System.out.println("‐‐‐‐‐‐");
        }
        // 释放资源
        br.close();
    }
}
// public void newLine

public class BufferedWriterDemo throws IOException {
    public static void main(String[] args) throws IOException  {
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
        // 写出数据
        bw.write("张三");
        // 写出换行
        bw.newLine();
        bw.write("李四");
        bw.newLine();
        bw.write("王五");
        bw.newLine();
        // 释放资源
        bw.close();
    }
}

转换流

字符编码和字符集

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

  • 字符编码 Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。
  • 字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

常见字符集有ASCII字符集GBK字符集Unicode字符集等。

编码引出的问题

在IDEA中,使用 FileReader 读取项目中的文本文件。由于IDEA的设置,都是默认的 UTF-8 编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。

InputStreamReader类

转换流 java.io.InputStreamReader ,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

  • InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

OutputStreamWriter类

转换流 java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName) : 创建一个指定字符集的字符流。
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");

序列化流

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据 、 对象的类型 和 对象中存储的属性 等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中存储的数据 信息,都可以用来在内存中创建对象。看图理解序列化:

ObjectOutputStream类

java.io.ObjectOutputStream类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

  • public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStreamObjectOutputStream
FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

序列化操作

  1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
  1. 写出对象方法
    public final void writeObject (Object obj) : 将指定的对象写出。
public class SerializeDemo {
    public static void main(String[] args) {
        Employee e = new Employee();
        e.name = "zhangsan";
        e.address = "beiqinglu";
        e.age = 20;
        try {
            // 创建序列化流对象
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
            // 写出对象
            out.writeObject(e);
            // 释放资源
            out.close();
            // 姓名,地址被序列化,年龄没有被序列化。
            System.out.println("Serialized data is saved");
        } catch (IOException i) {
            i.printStackTrace();
        }
    }
}

ObjectInputStream类

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

  • public ObjectInputStream(InputStream in) : 创建一个指定InputStreamObjectInputStream
  • public final Object readObject () : 读取一个对象。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;

public class SerializeDemo2 {
    public static void main(String[] args) {
        Employee e = null;
        try {
            // 创建反序列化流
            FileInputStream fileIn = new FileInputStream("employee.txt");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            // 读取一个对象
            e = (Employee) in.readObject();
            // 释放资源
            in.close();
            fileIn.close();
        } catch (IOException i) {
            // 捕获其他异常
            i.printStackTrace();
            return;
        } catch (ClassNotFoundException c) {
            // 捕获类找不到异常
            System.out.println("Employee class not found");
            c.printStackTrace();
            return;
        }
        // 无异常,直接打印输出
        System.out.println("Name: " + e.name);
        System.out.println("Address: " + e.address);
        System.out.println("age: " + e.age);
    }
}

Name: zhangsan
Address: beiqinglu
age: 0

age为瞬态, 未被序列化

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个ClassNotFoundException 异常。
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型
  • 该类没有可访问的无参数构造方法

Serializable (标识型)接口给需要序列化的类,提供了一个序列版本号。 serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。
如果没有serialVersionUID, 那么类只要重新编译, 原来序列化的数据就无法反序列化了,因为每次编译都会自动随机产生一个serialVersionUID, 如果加了, 就算重新编译, 依然可以反序列化

public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name;
     public String address;
     // 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
     public int eid; 
 
     public void addressCheck() {
         System.out.println("Address  check : " + name + " ‐‐ " + address);
     }
}

瞬时态(transient)和静态(static)修饰的变量不会被序列化, 因为静态变量不属于某个对象, 是所有对象共享的. 而序列化是保存对象的状态信息, 指每个对象独立的信息.

打印流

平时我们在控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于java.io.PrintStream 类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

PrintStream类

  • public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

System.out 就是 PrintStream 类型的,只不过它的流向是系统规定的,打印在控制台上。

属性集

java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时, System.getProperties 方法就是返回一个 Properties 对象。

Properties类

  • public Properties(): 创建一个空的属性列表
  • public Object setProperty(String key, String value) : 保存一对属性。
  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。
  • public Set<String> stringPropertyNames() :所有键的名称的集合。
  • public void load(InputStream inStream) : 从字节输入流中读取键值对。

文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

网络编程

三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
第三次握手,客户端再次向服务器端发送确认信息,确认连接。

TCP

  1. 客户端: java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  2. 服务端: java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

Socket类

  • public Socket(String host, int port): 创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的hostnull,则相当于指定地址为回送地址。
  • public InputStream getInputStream() : 返回此套接字的输入流。

    如果此Scoket具有相关联的通道,则生成的InputStream 的所有操作也关联该通道。关闭生成的InputStream也将关闭相关的Socket

  • public OutputStream getOutputStream() : 返回此套接字的输出流。

    如果此Scoket具有相关联的通道,则生成的OutputStream 的所有操作也关联该通道。关闭生成的OutputStream也将关闭相关的Socket

  • public void close() :关闭此套接字。

    一旦一个socket被关闭,它不可再使用。关闭此socket也将关闭相关的InputStreamOutputStream

  • public void shutdownOutput() : 禁用此套接字的输出流。

    任何先前写出的数据将被发送,随后终止输出流。

ServerSocket类

  • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。
  • public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

流程

  1. 【服务端】启动,创建ServerSocket对象,等待连接。
  2. 【客户端】启动,创建Socket对象,请求连接。
  3. 【服务端】接收连接,调用accept方法,并返回一个Socket对象。
  4. 【客户端】Socket对象,获取OutputStream,向服务端写出数据。
  5. 【服务端】Scoket对象,获取InputStream,读取客户端发送的数据。
  6. 【服务端】Socket对象,获取OutputStream,向客户端回写数据。
  7. 【客户端】Scoket对象,获取InputStream,解析回写数据。
  8. 【客户端】释放资源,断开连接。
//服务端
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;


public class ServerDemo {

    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动, 等待连接......");
        ServerSocket serverSocket = new ServerSocket(9999);

        Socket accept = serverSocket.accept();

        InputStream inputStream = accept.getInputStream();
        System.out.println("accept = " + accept);

        byte[] bytes = new byte[1024];
        int read = inputStream.read(bytes);
        System.out.println(new String(bytes));

        inputStream.close();
        serverSocket.close();

        System.out.println("服务端关闭......");

    }
}
//客户端
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动");
        Socket socket = new Socket("127.0.0.1", Integer.parseInt("9999"));

        OutputStream os = socket.getOutputStream();
        os.write("hello".getBytes());

        os.close();
        socket.close();

    }
}
//初始化时已进行 connect
    private Socket(SocketAddress address, SocketAddress localAddr,
                   boolean stream) throws IOException {
        setImpl();

        // backward compatibility
        if (address == null)
            throw new NullPointerException();

        try {
            createImpl(stream);
            if (localAddr != null)
                bind(localAddr);
            connect(address);
        } catch (IOException | IllegalArgumentException | SecurityException e) {
            try {
                close();
            } catch (IOException ce) {
                e.addSuppressed(ce);
            }
            throw e;
        }
    }

UDP

jdk8

函数式接口

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的 public abstract 是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {    
    void myMethod();
}

@FunctionalInterface注解

@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

自定义函数式接口

public class Demo09FunctionalInterface {    
    // 使用自定义的函数式接口作为方法参数
    private static void doSomething(MyFunctionalInterface inter) {
        inter.myMethod(); // 调用自定义的函数式接口方法
    }
    
    public static void main(String[] args) {
        // 调用使用函数式接口的方法
        doSomething(() ‐> System.out.println("Lambda执行啦!"));
    }
}

函数式编程

Lambda的延迟执行

有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。

性能浪费的日志案例

日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:

public class Demo01Logger {
    private static void log(int level, String msg) {
        if (level == 1) {
            System.out.println(msg);
        }
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(1, msgA + msgB + msgC);
    }
}

这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。

备注:SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS") ,其中的大括号 {} 为占位符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。

Lambda的更优写法

@FunctionalInterface
public interface MessageBuilder {  
    String buildMessage();
}
public class Demo02LoggerLambda {
    private static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            System.out.println(builder.buildMessage());
        }
    }

    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(1, () ‐> msgA + msgB + msgC );
    }
}

证明Lambda的延迟

public class Demo03LoggerDelay {
    private static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            System.out.println(builder.buildMessage());
        }
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
 
        log(2, () ‐> {
            System.out.println("Lambda执行!");
            return msgA + msgB + msgC;
        });
    }
}

从结果中可以看出,在不符合级别要求的情况下,Lambda将不会执行。从而达到节省性能的效果。

实际上使用内部类也可以达到同样的效果,只是将代码操作延迟到了另外一个对象当中通过调用方法来完成。而是否调用其所在方法是在条件判断之后才执行的。

使用Lambda作为参数和返回值

如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。

java.lang.Runnable 接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。

public class Demo04Runnable {
    private static void startThread(Runnable task) {
        new Thread(task).start();
    }
 
    public static void main(String[] args) {
        startThread(() ‐> System.out.println("线程任务执行!"));
    }
}

如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一
个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。

import java.util.Arrays;
import java.util.Comparator;
 
public class Demo06Comparator {
    private static Comparator<String> newComparator() {
        return (a, b) ‐> b.length() ‐ a.length();
    }
 
    public static void main(String[] args) {
        String[] array = { "abc", "ab", "abcd" };
        System.out.println(Arrays.toString(array));
        Arrays.sort(array, newComparator());
        System.out.println(Arrays.toString(array));
    }
}

直接return一个Lambda表达式即可

常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。

Supplier接口

java.util.function.Supplier<T>接口仅包含一个无参的方法:T get() 。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

import java.util.function.Supplier;
 
public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
        return function.get();
    }
 
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() ‐> msgA + msgB));
    }
}

使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。

public class Demo02Test {
    //定一个方法,方法的参数传递Supplier,泛型使用Integer
    public static int getMax(Supplier<Integer> sup){
        return sup.get();
    }
 
    public static void main(String[] args) {
        int arr[] = {2,3,4,52,333,23};
 
        //调用getMax方法,参数传递Lambda
        int maxNum = getMax(()‐>{
           //计算数组的最大值
           int max = arr[0];
           for(int i : arr){
               if(i>max){
                   max = i;
               }
           }
           return max;
        });
        System.out.println(maxNum);
    }
}

Consumer接口

java.util.function.Consumer<T> 接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。

抽象方法:accept

Consumer 接口中包含抽象方法 void accept(T t),意为消费一个指定泛型的数据。

import java.util.function.Consumer;

public class ConsumerDemo {
    private static void consumeString(String name, Consumer<String> function) {
        function.accept(name);
    }

    public static void main(String[] args) {
        consumeString("张三", (String name)->{
            System.out.println("name = " + name);

            System.out.println("new = " + new StringBuilder(name).reverse().toString());
        });
    }
}

默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

java.util.ObjectsrequireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。

import java.util.function.Consumer;


public class ConsumerDemo2 {
    private static void consumeString(Consumer<String> one, Consumer<String> two) {
        one.andThen(two).accept("Hello");
    }

    public static void main(String[] args) {
        consumeString(
                s -> System.out.println(s.toUpperCase()),
                s -> System.out.println(s.toLowerCase()));
    }
}

格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX. ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。

public static void main(String[] args) {
    String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
}
import java.util.function.Consumer;

public class DemoConsumer {
    public static void main(String[] args) {
        String[] array = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
        printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
                s -> System.out.println("。性别:" + s.split(",")[1] + "。"),
                array);
    }

    private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
        for (String info : array) {
            one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
        }
    }
}


姓名:迪丽热巴。性别:女。
姓名:古力娜扎。性别:女。
姓名:马尔扎哈。性别:男。

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用
java.util.function.Predicate<T> 接口。

抽象方法:test

Predicate 接口中包含一个抽象方法: boolean test(T t) 。用于条件判断的场景:

import java.util.function.Predicate;

public class PredicateDemo {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5);
    }
}

默认方法:and

既然是条件判断,就会存在三种常见的逻辑关系。其中将两个 Predicate 条件使用“”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:

import java.util.function.Predicate;
 
public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.and(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

默认方法:or

and 的“”类似,默认方法 or 实现逻辑关系中的“”。JDK源码为:

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:

import java.util.function.Predicate;
 
public class Demo16PredicateAnd {
    private static void method(Predicate<String> one, Predicate<String> two) {
        boolean isValid = one.or(two).test("Helloworld");
        System.out.println("字符串符合要求吗:" + isValid);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.contains("H"), s ‐> s.contains("W"));
    }
}

默认方法:negate

”、“”已经了解了,剩下的“(取反)也会简单。默认方法 negate 的JDK源代码为:

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在 test 方法调用之前调用 negate 方法,正如 andor 方法一样:

import java.util.function.Predicate;
 
public class Demo17PredicateNegate {
    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.negate().test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }
 
    public static void main(String[] args) {
        method(s ‐> s.length() < 5);
    }
}

Function接口

java.util.function.Function<T,R> 接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。

抽象方法:apply

Function 接口中最主要的抽象方法为: R apply(T t) ,根据类型T的参数获取类型R的结果使用的场景例如:将 String 类型转换为 Integer 类型。

import java.util.function.Function;

public class FunctionDemo {
    private static void method(Function<String, Integer> function) {
        int num = function.apply("10");
        System.out.println(num + 20);
    }

    public static void main(String[] args) {
        method(s -> Integer.parseInt(s)*2);
    }
}

默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如:

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

import java.util.function.Function;
 
public class Demo12FunctionAndThen {
    private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
        int num = one.andThen(two).apply("10");
        System.out.println(num + 20);
    }
 
    public static void main(String[] args) {
        method(str‐>Integer.parseInt(str)+10, i ‐> i *= 10);
    }
}

第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起。

Function的前置条件泛型和后置条件泛型可以相同。

Stream流

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How)

import java.util.ArrayList;
import java.util.List;

public class StreamDemo1 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        list.stream()
                .filter(s -> s.startsWith("张"))
                .filter(s -> s.length() == 3)
                .forEach(System.out::println);
    }
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流过滤姓张过滤长度为3逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。

这里的 filtermapskip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count 执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

Stream(流)是一个来自数据源的元素队列

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源 流的来源。 可以是集合,数组 等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者增强for的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式,流可以直接调用遍历方法。

当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)数据转换执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

获取流

java.util.stream.Stream<T> 是Java 8新加入的最常用的流接口。
获取一个流非常简单,有以下几种常用的方式:

  • 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  • Stream 接口的静态方法 of 可以获取数组对应的流。

根据Collection获取流

java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。

import java.util.*;
import java.util.stream.Stream;
 
public class Demo04GetStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();
 
        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();
 
        Vector<String> vector = new Vector<>();
        // ...
        Stream<String> stream3 = vector.stream();
    }
}

根据Map获取流

java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分keyvalueentry等情况:

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
 
public class Demo05GetStream {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        // ...
        Stream<String> keyStream = map.keySet().stream();
        Stream<String> valueStream = map.values().stream();
        Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
    }
}

根据数组获取流

import java.util.stream.Stream;

public class StreamDemo2 {
    public static void main(String[] args) {
        String[] arr = {"赵", "钱", "孙", "李"};
        Stream<String> arr1 = Stream.of(arr);
        arr1.forEach(System.out::println);
    }
}

of 方法的参数其实是一个可变参数,所以支持数组。

    public static<T> Stream<T> of(T... values) {
        return Arrays.stream(values);
    }

常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
  • 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。

forEach

void forEach(Consumer<? super T> action);

该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。

java.util.function.Consumer<T>接口是一个消费型接口。
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

filter

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda方法引用)作为筛选条件。

java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为
boolean test(T t);

该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。

map

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

java.util.stream.Function 函数式接口,其中唯一的抽象方法为
R apply(T t);

可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

count

流提供 count 方法来数一数其中的元素个数:

long count();

limit

limit 方法可以对流进行截取,只取用前n个。

Stream<T> limit(long maxSize);

skip

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

concat

如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。

方法引用

  • Lambda表达式写法: s -> System.out.println(s)
  • 方法引用写法: System.out::println

通过对象名引用成员方法

对象存在, 方法也存在

public class MethodRefObject {
    void printUpperCase(String str){
        System.out.println("str.toUpperCase() = " + str.toUpperCase());
    }
}


@FunctionalInterface
public interface Printable {
    void print(String str) ;
}


public class StreamDemo3 {
    static void printString(Printable pt){
        pt.print("hello");
    }

    public static void main(String[] args) {
        MethodRefObject refObject = new MethodRefObject();
        printString(s->refObject.printUpperCase(s));
        printString(refObject::printUpperCase);
    }
}

通过类名称引用静态方法

@FunctionalInterface
public interface Calcable {
    int calc(int num);
}

public class Demo06MethodRef {
    private static void method(int num, Calcable lambda) {
        System.out.println(lambda.calc(num));
    }
 
    public static void main(String[] args) {
        method(‐10, Math::abs);
    }
}

通过super引用成员方法

public class Man extends Human {
    @Override
    public void sayHello() {
        System.out.println("大家好,我是Man!");
    }
 
    //定义方法method,参数传递Greetable接口
    public void method(Greetable g){
        g.greet();
    }
 
    public void show(){
        method(super::sayHello);
    }
}

通过this引用成员方法

@FunctionalInterface
public interface Richable {
    void buy();
}

public class Husband {
    private void buyHouse() {
        System.out.println("买套房子");
    }
 
    private void marry(Richable lambda) {
        lambda.buy();
    }
 
    public void beHappy() {
        marry(this::buyHouse);
    }
}

类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示

public class Person {
    private String name;
 
    public Person(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
public interface PersonBuilder {
    Person buildPerson(String name);
}
public class Demo10ConstructorRef {
    public static void printName(String name, PersonBuilder builder) {
        System.out.println(builder.buildPerson(name).getName());
    }
 
    public static void main(String[] args) {
        printName("赵丽颖", Person::new);
    }
}

数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器

@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}
public class Demo12ArrayInitRef {
    private static int[] initArray(int length, ArrayBuilder builder) {
        return builder.buildArray(length);
    }
 
    public static void main(String[] args) {
        int[] array = initArray(10, int[]::new);
    }
}

反射

获取Class对象的方式

  1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
    • 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
  2. 类名.class:通过类名的属性class获取
    • 多用于参数的传递
  3. 对象.getClass():getClass()方法在Object类中定义着。
    • 多用于对象的获取字节码的方式
  • 结论:
    同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

Class对象功能

  1. 获取成员变量们

    • Field[] getFields() :获取所有 public 修饰的成员变量
    • Field getField(String name) 获取指定名称的 public 修饰的成员变量
    • Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
    • Field getDeclaredField(String name)
  2. 获取构造方法们

    • Constructor<?>[] getConstructors()
    • Constructor<T> getConstructor(类<?>... parameterTypes)
    • Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
    • Constructor<?>[] getDeclaredConstructors()
  3. 获取成员方法们:

    • Method[] getMethods()
    • Method getMethod(String name, 类<?>... parameterTypes)
    • Method[] getDeclaredMethods()
    • Method getDeclaredMethod(String name, 类<?>... parameterTypes)
  4. 获取全类名

    • String getName()

Field:成员变量

  1. 设置值

    • void set(Object obj, Object value)
  2. 获取值

    • get(Object obj)
  3. 忽略访问权限修饰符的安全检查

    • setAccessible(true):暴力反射

Constructor:构造方法

  • T newInstance(Object... initargs)
  • 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
    public static void main(String[] args) throws Exception {
        // 获取class对象
        Class<Person> personClass = Person.class;
        System.out.println("personClass = " + personClass);

        // 获取构造方法
        Constructor<Person> personConstructor = personClass.getConstructor(String.class, int.class);
        System.out.println("personConstructor = " + personConstructor);

        // 实例化对象
        Person person = personConstructor.newInstance("张三", 20);
        System.out.println("person = " + person);

        System.out.println("-----------------空参数构造器----------------------");

        // 获取构造方法(空参)
        Constructor<Person> personConstructor1 = personClass.getConstructor();
        System.out.println("personConstructor1 = " + personConstructor1);

        // 实例化对象
        Person person1 = personConstructor1.newInstance();
        System.out.println("person1 = " + person1);

        System.out.println("------------------------简化操作---------------------");
        // 简化操作
        Person person2 = personClass.newInstance();
        System.out.println("person2 = " + person2);


personClass = class com.gao.Person
personConstructor = public com.gao.Person(java.lang.String,int)
person = Person{name='张三', age=20}
-----------------空参数构造器----------------------
personConstructor1 = public com.gao.Person()
person1 = Person{name='null', age=0}
------------------------简化操作---------------------
person2 = Person{name='null', age=0}

Method:方法对象

  • 执行方法:Object invoke(Object obj, Object... args)
  • 获取方法名称:String getName

demo

className=Person
methodName=toString
    public static void main(String[] args) throws Exception {
        // 1. 加载配置文件
        // 1.1 创建Properties对象
        Properties properties = new Properties();

        // 1.2 加载配置文件, 转换为集合
        // 1.2.1 获取配置文件
        ClassLoader classLoader = RefTest.class.getClassLoader();
        InputStream asStream = classLoader.getResourceAsStream("pro.properties");
        properties.load(asStream);

        // 2. 获取配置文件中定义的数据
        String className = properties.getProperty("className");
        String methodName = properties.getProperty("methodName");

        // 3. 加载类进内存
        Class<?> cls = Class.forName(className);

        // 4. 创建对象
        Constructor<?> constructor = cls.getConstructor();
        Object obj = constructor.newInstance();

        // 5. 获取方法对象
        Method clsMethod = cls.getMethod(methodName);

        // 6. 执行方法
        Object invokenvokeMethod = clsMethod.invoke(obj);

        System.out.println("invokenvokeMethod = " + invokenvokeMethod);

    }

invokenvokeMethod = Person{name='null', age=0}    

注解

注解(Annotation),也叫元数据。一种代码级别的说明。与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释.

作用分类:

① 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】
② 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
③ 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】

JDK中预定义的一些注解

  • @Override :检测被该注解标注的方法是否是继承自父类(接口)的
  • @Deprecated:该注解标注的内容,表示已过时
  • @SuppressWarnings:压制警告
    • 一般传递参数all @SuppressWarnings("all")

自定义注解

格式:

    @元注解
    【修饰符】 @interface 注解名{
        配置参数列表
    }
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
  • 本质:注解本质上就是一个接口,该接口默认继承Annotation接口

    • public interface MyAnno extends java.lang.annotation.Annotation {}
  • 属性:接口中的抽象方法

    • 要求:
      1. 属性的返回值类型有下列取值

        • 基本数据类型
        • String
        • 枚举
        • 注解
        • 以上类型的数组
            public @interface MyAnno {
                int func1() default 1;
                String func2();
                Type func3();
                MyAnno2 func4();
            }
        
      2. 定义了属性,在使用时需要给属性赋值

        1. 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
        2. 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
        3. 数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略

元注解:用于描述注解的注解

  • @Target:描述注解能够作用的位置
    • ElementType取值:
      • TYPE:可以作用于类上
      • METHOD:可以作用于方法上
      • FIELD:可以作用于成员变量上
      • . . .
  • @Retention:描述注解被保留的阶段(SOURCE, CLASS, RUNTIME)
    • @Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到
  • @Documented:描述注解是否被抽取到api文档中
  • @Inherited:描述注解是否被子类继承

在程序使用(解析)注解:获取注解中定义的属性值

  1. 获取注解定义的位置的对象 (Class,Method,Field)
  2. 获取指定的注解
    • getAnnotation(Class)
          //其实就是在内存中生成了一个该注解接口的子类实现对象
          public class ProImpl implements Pro{
              public String className(){
                  return "className";
              }
              public String methodName(){
                  return "methodName";
              }
          }
      
  3. 调用注解中的抽象方法获取配置的属性值

demo

package com.anno;

public @interface MyAnno {
}

反编译结果

Compiled from "MyAnno.java"
public interface com.anno.MyAnno extends java.lang.annotation.Annotation {
}

替换反射

import	java.lang.annotation.RetentionPolicy;
import	java.lang.annotation.Retention;
import	java.lang.annotation.ElementType;
import	java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ProAnno {
    String className();
    String methodName();
}
@ProAnno(className = "Person", methodName = "toString")
public class RefTest2 {
    public static void main(String[] args) throws Exception {
        ProAnno proAnno = RefTest2.class.getAnnotation(ProAnno.class);
        System.out.println("proAnno = " + proAnno);

        String className = proAnno.className();
        String methodName = proAnno.methodName();

        System.out.println("className = " + className);
        System.out.println("methodName = " + methodName);

        Class<?> cls = Class.forName(className);
        Object newInstance = cls.getConstructor(String.class, int.class).newInstance("张三", 20);

        Method method = cls.getMethod(methodName);

        Object o = method.invoke(newInstance);
        System.out.println("o = " + o);
    }
}
posted @ 2019-10-10 17:29  寒菱  阅读(227)  评论(0编辑  收藏  举报