JavaSE基础 (全网最全知识点)

背景介绍

java运行机理(即使编译型语言,又是解释型语言)

image

编译型语言(如:c语言)

源代码需要通过预编译形成可执行文件,再由系统执行该文件形成可识别的二进制文件

image

image

解释型语言

边执行边转换。源代码先翻译成中间代码,解释器(类似于JVM)再对中间代码进行解释运行,每执行一次都要翻译一次。

image

image

标识符规则:

  • 标识符只能由大小写字母、数字、下划线(_)和美元符号($)组成,但是不能以数字开头

  • 大小写敏感

  • 不能与Java语言的关键字重名

    【例:类型名:final,new,class,static(用于定义变量、方法、类的类型名);跳转语言:break、throw、for】

  • 不可以是 true 和 false (true、false不是关键字)

  • 一般采用驼峰命名法

驼峰命名法:

包名:xxxyyyzzz

类名、接口名:XxxYyyZzz

变量名、方法名:xxxYyyZzz

常量名:XXX_YYY_ZZZ

注释:

//我是单行注释

/**
* 我是
* 多行注释
*/

//TODO 待做标记

二进制

字节

一个位也叫一个bit,8个bit称为1字节,16个bit称为一个字,32个bit称为一个双字,64个bit称为一个四字

二进制转换

https://www.cnblogs.com/buchizicai/p/15866145.html

计算机的加减法

以8bit(一个字节为例)

原码

  • 最高位为符号位
  • 其余位用于表示二进制的数字

例如:1:00000001 -1:10000001

反码

由于原码计算麻烦,所以有了反码

  • 正数:反码是其本身
  • 负数:反码是负数原码的基础上,符号位不变,其余各位取反

例如:-1:11111110

补码

由于反码有+0和-0之分,so有了补码(java用的就是补码)

  • 正数:补码就是其本身
  • 负数:补码是负数原码的基础上,符号位不变,其余各位取反,再加上1(就是在反码基础上加1)

以4bit为例:+0:(1)0000 【1溢出了舍去】,-8:1000

进制圈⚪

当规定了一个变量的类型,如byte(-128—127),那最大表示的数127再加1,得到的就是-128了。同理,-128在减1,得到的就是127了。

数据类型

整数类型

byte—short—int—long—BigInteger

小数类型

float—double—BigDecimal

字符型(代表一个符号)

  • char 字符型(16个bit,也就是2字节,它不带符号!)范围是0 ~ 65535
  • 使用Unicode表示就是:\u0000 ~ \uffff
  • 字符要用单引号扩起来!比如 char c = '淦';

字符其实本质也是数字,但是这些数字通过编码表进行映射,代表了不同的字符,比如字符'A'的ASCII码就是数字65,所以char类型其实可以转换为上面的整数类型。

Java的char采用Unicode编码表(不是ASCII编码!)

Unicode与Ascii区别:Unicode编码表包含ASCII的所有内容,同时还包括了全世界的语言,ASCII只有1字节,而Unicode编码是2字节,能够代表65536种文字,足以包含全世界的文字了!(我们编译出来的字节码文件也是使用Unicode编码的,所以利用这种特性,其实Java支持中文变量名称、方法名称甚至是类名)

数据类型转换

以下都是自动转换,非自动转换就需要强制转换。如:字符串转整数:Integer.parseInt(String s);

隐式类型转换

隐式类型转换支持字节数小的类型自动转换为字节数大的类型,整数类型自动转换为小数类型,转换规则如下:【小范围转大范围】

  • byte→short(char)→int→long→float→double

问题:为什么long比float大,还能转换为float呢?小数的存储规则让float的最大值比long还大,只是可能会丢失某些位上的精度!

int a=100;
long b=a;
System.out.println(b);

//输出100

显式类型转换

也叫强转换类型,牺牲精度强行进行类型转换 【大范围转小范围】

int i = 128;
byte b = (byte)i;
System.out.println(b);

//输出 -128【原因:127+1=-128】

float a=1.01;
int b = a;
System.out.println(b);

//输出 1

Object a="hello";
String b =(String) a;	//此时必须强转,因为提供的是Object而要求接收到的是String

数据类型自动提升

在参与运算时(也可以位于表达式中时,自增自减除外),所有的byte型、short型和char的值将被提升到int型:

byte b = 105;
b = b + 1;   //报错:左边要求接受byte型,而右边提供int型
System.out.println(b);

这个特性是由 Java虚拟机规范 定义的,也是为了提高运行的效率。其他的特性还有:

  • 如果一个操作数是long型,计算结果就是long型
  • 如果一个操作数是float型,计算结果就是float型
  • 如果一个操作数是double型,计算结果就是double型

运算符

加号

  • 拼接作用:字符串+数字,结果是字符串与数字的拼接(因为此时数字被当作字符串看待)

逻辑运算符

  • && 、& 与运算,要求两边同时为true才能返回true;

&和&&做逻辑与时的区别

  • &会判断两边的true or false
  • &&当判断左边为false时将不再判断右边
  • |、|| 或运算,要求两边至少要有一个为true才能返回true

|和||做逻辑与时的区别

  • |会判断两边的true or false
  • ||当判断左边为true时将不再判断右边
  • ! 非运算,一般放在表达式最前面,表达式用括号扩起来,表示对表达式的结果进行反转

位运算

注意:返回的是运算后的同类型值,不是boolean!

  • & 按位与(与1得1)
  • | 按位或(与0得0)
  • ^ 按位异或 0 ^ 0 = 0(相异得1,相同得0)
  • ~ 按位非

三目运算符

int a = 7, b = 15;
String str = a > b ? "行" : "不行";  // 判断条件(只能是boolean,或返回boolean的表达式) ? 满足的返回值 : 不满足的返回值 
System.out.println("汉堡做的行不行?"+str);  //汉堡做的行不行?不行

方法

方法的重载

定义:方法名相同,但参数不同(可以是参数个数、类型、返回类型不同,但不可以仅返回类型不同!)

构造方法

构造方法(构造器)没有返回值,也可以理解为,返回的是当前对象的引用!每一个类都默认自带一个无参构造方法(如果设置了有参构造,那默认的无参构造就被覆盖了)

静态变量和静态方法

可以理解为:静态是整个项目的全局变量,可以被直接调用,可以被对象调用,但改变的是同一个变量的值

静态变量和静态方法是类具有的属性(后面还会提到静态类、静态代码块),也可以理解为是所有对象共享的内容。我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。那么,一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。

类加载机制

类并不是在一开始就全部加载好,而是在需要时才会去加载(提升速度)以下情况会加载类:

  • 访问类的静态变量,或者为静态变量赋值
  • new 创建类的实例(隐式加载)
  • 调用类的静态方法
  • 子类初始化时
  • 其他的情况会在讲到反射时介绍

所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载

public class Student {
    static int a = test();  //直接调用静态方法,只能调用静态方法

    Student(){
        System.out.println("构造类对象");
    }

    static int test(){   //静态方法刚加载时就有了
        System.out.println("初始化变量a");
        return 1;
    }
}

思考:下面这种情况下,程序能正常运行吗?如果能,会输出什么内容?

public class Student {
    static int a = test();

    static int test(){
        return a;
    }

    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

//输出:0

解析:定义和赋值是两个阶段,在定义时会使用默认值(上面讲的,类的成员变量会有默认值)定义出来之后,如果发现有赋值语句,再进行赋值,而这时,调用了静态方法,所以说会先去加载静态方法,静态方法调用时拿到a,而a这时仅仅是刚定义,所以说还是初始值,最后得到0【结论:在定义变量时,会赋予默认值(一般是0),然后再判断是否有赋值语句,有的话再替换默认值】

代码块和静态代码块

代码块是在 调用该代码块所属的类对象创建时才被加载(普通成员变量也是如此);

静态代码块是在 调用该代码块所属的类刚加载时,就被调用;

代码块在对象创建时执行,也是属于类的内容,但是它在构造方法执行之前执行(和成员变量初始值一样),且每创建一个对象时,只执行一次!(相当于构造之前的准备工作)

public class Student {
    {
        System.out.println("我是代码块");
    }

    Student(){
        System.out.println("我是构造方法");
    }
}

静态代码块和上面的静态方法和静态变量一样,在类刚加载时就会调用;

public class Student {
    static int a;

    static {
        a = 10;
    }
    
    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

包和访问控制

包声明

包的命名:一般包按照个人或是公司域名的规则倒过来写 顶级域名.一级域名.二级域名 com.java.xxxx

包的导入

  1. 正常导入:import math.*

  2. 静态导入:

    静态导入可以直接导入某个类的静态方法或者是静态变量,导入后,相当于这个方法或是类在定义在当前类中,可以直接调用该方法。

    import static com.test.ui.Student.test;
    
    public class Main {
        public static void main(String[] args) {
            test();
        }
    }

    注:静态导入不会进行类的初始化/加载!

访问控制

可作用于方法、变量上。(创建方法变量等默认是default,不用特意写出来)

image

和文件名称相同的类,只能是public,并且一个java文件中只能有一个public class!

// Student.java
public class Student {
    
}
class Test{   //不能添加权限修饰符!只能是default
	
}

注:类只能的public、default,当类是private时是内部类。public类在一个文件中有且仅有一个

可变长参数

可变长参数实质就是数组的一种应用,我们可以指定方法的形参为一个可变长参数,要求实参可以根据情况动态填入0个或多个,而不是固定的数量【由于可变长参数实质是数组,所以传入的实参只能是同一数据类型】

public static void main(String[] args) {
     test("AAA", "BBB", "CCC");    //可变长,最后都会被自动封装成一个数组
}
    
private static void test(String... test){
     System.out.println(test[0]);    //其实参数就是一个数组
}

当想要传入的参数部分是对应的定长参数,部分是不定长参数,需要如下↓

public static void  main(Stirng [] args){
	test(10,"AAA","BBB","CCC");		//10是定长,后面部分是不定长会被封装到一个数组里
}
private static void test(int n, String... test){
    System.out.println(n+test[0]);    //其实可变长参数就是一个数组
}

封装、继承、多态

封装、继承和多态是面向对象编程的三大特性。

封装

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心如何实现。外界只能调用接口or使用该方法,这样将操作成员变量的权限与外界隔开。

目的:是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个代码带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要同getter和setter方法来查看和设置变量。【小结:成员变量应该私有化(private),使外部只能通过getter、setter方法来查看和设置变量】

例子:学生小明已经创建成功,正常情况下能随便改他的名字和年龄吗?

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

拓展:外部现在只能通过调用我定义的方法来获取成员属性,而我们可以在这个方法中进行一些额外的操作,比如小明可以修改名字,但是名字中不能包含"小"这个字。【再设置变量的时候增加设置条件,如:电话号码必须11位数字】

public void setName(String name) {
    if(name.contains("小")) return;
    this.name = name;
}

继承

在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。

例子:现在学生分为两种,艺术生和体育生,他们都是学生的分支,但是他们都有自己的方法:

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

public class SportsStudent extends Student{   //通过extends关键字来继承父类

    public SportsStudent(String name, int age) {
        super(name, age);   //必须先通过super关键字(指代父类),实现父类的构造方法!
    }

    public void exercise(){
        System.out.println("我超勇的!");
    }
}

public class ArtStudent extends Student{

    public ArtStudent(String name, int age) {
        super(name, age);
    }

    public void art(){
        System.out.println("随手画个毕加索!");
    }
}

继承特点:

  • 子类具有父类的全部属性(包括private属性,私有不能直接使用,但可以通过反射使用),同时子类还能有自己的方法。

  • 继承只能继承一个父类,不支持多继承!

  • 调用父类的方法和变量super.way()

  • 子类方法中调用变量的优先级:形参列表中 > 当前类的成员变量 > 父类成员变量

    public void setTest(int test){
        test = 1;
      	this.test = 1;
      	super.test = 1;
    }
  • 每一个子类必须定义一个实现父类构造方法的构造方法,也就是需要在构造方法第一行使用super(),如果父类使用的是默认构造方法,那么子类不用手动指明。

    public class Student {
        private String name;
        private int age;
        
        public Student(){};	//可以省略
    }
    
    public class SportStudent extends Student(){
        SportStudent(){	//如果是无参构造可以省略,若是有参构造则不可以省略,并且super()必须在构造函数第一行执行!
            super();	//无参情况下可以省略
        }
    }
    
    
    
    public class Student {
        private String name;
        private int age;
      
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    
    public class ArtStudent extends Student(){
        ArtStudent(String name,String age){
            super(name,age);	//子类有参构造,第一行必须先实现父类的任一构造函数
        }
    }

多态

多态是同一个行为具有多个不同表现形式或形态的能力。白话:同样的方法,由于实现类不同,执行的结果也不同!

方法的重写

重载:原有方法逻辑不变,支持更多参数实现。(方法名相同,参数个数类型不同)

重写:子类重写(覆盖)父类的方法

//父类中的study
public void study(){
    System.out.println("学习");
}

//子类中的study
@Override  //声明这个方法是重写的,但是可以不要,我们现阶段不接触
public void study(){
    System.out.println("给你看点好康的");
}

思考:静态方法能被重写吗?不能!【所以父类和子类重写的方法不能加static,加了就不是重写了】

类的类型转换

类也是支持类型转换的(仅限于存在亲缘关系的类之间进行转换)比如子类可以直接向上转型:【小范围转大范围】

Student student = new SportsStudent("lbw", 20);  //父类变量引用子类实例
student.study();     //得到依然是具体实现(父类Student)的结果,而不是当前类型的结果

我们也可以把已经明确是由哪个类实现的父类引用,强制转换为对应的类型,这叫向下转型:【大范围转小范围就需要强制转换,条件是必须是对应的子类,不能是别的子类】

Student student = new SportsStudent("lbw", 20);  //是由SportsStudent进行实现的
//... do something...

SportsStudent ps = (SportsStudent)student;  //让它变成一个具体的子类。只能转SportStudent,不能是ArtStudent,因为student是SportStudent转来的

ps.sport();  //调用具体实现类的方法

instanceof关键字

A instanceof B:A这个类是不是B这个类的类型,返回Boolean型

那么我们如果只是得到一个父类引用,但是不知道它到底是哪一个子类的实现怎么办?我们可以使用instanceof关键字来实现,它能够进行类型判断!

private static void test(Student student){
    if (student instanceof SportsStudent){
        SportsStudent sportsStudent = (SportsStudent) student;
        sportsStudent.sport();
    }else if (student instanceof ArtStudent){
        ArtStudent artStudent = (ArtStudent) student;
        artStudent.art();
    }
}

思考:student instanceof Student的结果是什么?true

(因为Student是SportStudent和ArtStudent的父类,严格来讲SportStudent和ArtStudent也是Student类)

final关键字

可以添加到 类、方法、变量前。一旦添加后,一次赋值后就不可更改,类不可被继承、方法不可被重写、变量不可被修改。

抽象类

翻译:在一个类里定义一些没有方法体的方法,必须由子类实现(子类可以继续写成抽象类,由最底层的子类实现)

注:类和方法都需要abstract修饰。

特点:辅助继承关系

public abstract class Student {    //抽象类
    int username;
    public abstract void test();  //抽象方法
}

抽象类由于不是具体的类定义,因此无法直接通过new关键字来创建对象!

Student s = new Student(){    //只能直接创建带实现的匿名内部类!
  public void test(){
    
  }
}

特点:抽象类一般只用作继承使用!抽象类使得继承关系之间更加明确:

public void study(){   //现在只能由子类编写,父类没有定义,更加明确了多态的定义!同一个方法多种实现!
    System.out.println("给你看点好康的");
}

接口

接口只代表功能,只包含方法的定义,实现接口意味着是实现该功能。

public interface Eat {
	void eat(); 
}
  • 接口只能包含public权限的抽象方法!我们可以通过声明default关键字来给抽象方法一个默认实现
public interface Eat {
    default void eat(){
        //do something...默认实现一些基本功能
    }
}
  • 接口中定义的变量,默认为public static final
public interface Eat {
    int a = 1;
    void eat();
}

实现接口的类也能通过instanceof关键字判断,也支持向上和向下转型!

接口和抽象类区别

  1. 抽象类要被子类继承,接口要被类实现
  2. 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现
  3. 接口是设计的结果,抽象类是重构的结果
  4. 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高
  5. 抽象类可以有具体的方法和属性(普通变量),接口只能有抽象方法和不可变常量(公共静态的常量)
  6. 抽象类主要用来抽象类别,接口主要用来抽象功能

内部类

成员内部类【了解】

我们的类中可以在嵌套一个类:

public class Test {
    class Inner{   //类中定义的一个内部类
        
    }
}

成员内部类和成员变量和成员方法一样,都是属于对象的,也就是说,必须存在外部对象,才能创建内部类的对象!

public static void main(String[] args) {
    Test test = new Test();
    Test.Inner inner = test.new Inner();   //写法有那么一丝怪异,但是没毛病!
}

静态内部类【了解】

静态内部类其实就和类中的静态变量和静态方法一样,是属于类拥有的,我们可以直接通过类名.去访问:

public class Test {
    static class Inner{

    }
}

public static void main(String[] args) {
    Test.Inner inner = new Test.Inner();   //不用再创建外部类对象了!
}

局部内部类【了解】

对,你没猜错,就是和局部变量一样哒~

public class Test {
    public void test(){
        class Inner{

        }
        
        Inner inner = new Inner();
    }
}

反正我是没用过!内部类 -> 累不累 -> 反正我累了!

匿名内部类

白话:在创建接口/类对象的时候重写方法,或者调用父类的方法,实现在创建对象的时候对对象初始化。

(也可以理解在写匿名内部类的时候,是对接口的实现/对父类的方法重写)

使用场景:一个接口/类的方法的某个实现方式在程序中只会执行一次

优点:对于使用次数少的方法,无需创造新的类,减少代码冗余

缺点:无法在创建的对象外复用,需要再次使用重写的方法需要重新在写一次

一般情况:一般使用一个功能需要,写接口然后再写实现类。在创建接口对象才能调用里面的方法。接口 对象名 = new 实现类 ();

使用匿名内部类:可以直接创建接口/类对象然后再创建的时候写一个只用一次的方法

举例:

创建对象时调用父类方法实现对象初始化:

List<Integer> list = new LinkedList<Integer>(){   //Java9才支持匿名内部类使用钻石运算符
    {
        this.add(10);
        this.add(2);
        this.add(5);
        this.add(8);
    }
};

接口情况:

//接口
public interface Interface01 {
    void show(String s);
}

//测试类
public test {
    public static void main(String[] args) {
        //写法一:实现接口的抽象方法
        new Interface01(){
            @Override
            public void show(String s) {
                System.out.println("我是一个" + s);
            }
        }.show("接口");
        
        
        //写法二:实现接口的抽象方法
        Interface01 test =  new Interface01(){
            @Override
            public void show(String s) {
                System.out.println("我是一个" + s);
            }
        }
        test.show("接口");
    }
}

类的情况:(具体类与抽象类一样)

//具体类
public class Class01 {
    public void show(String s){
        System.out.println("啦啦啦");
    }
}

//测试类
public static void main(String[] args) {
    //写法一:重写具体类的方法
    new Class01(){
        @Override
        public void show(String s) {
            System.out.println("我是一个" + s);
        }
    }.show("具体类");
    
    //写法二:重写具体类的方法
    Class01 test = new Class01(){
        @Override
            public void show(String s) {
                System.out.println("我是一个" + s);
            }
    }
    test.show("具体类");
}

lambda表达式

只适用于接口or抽象类中只有一个方法的情况。且只写参数与方法体

读作λ表达式,它其实就是我们接口匿名实现的简化,比如说:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

public static void main(String[] args) {
        Eat eat = (参数) -> {方法体};   //等价于上述内容
    }

lambda表达式(匿名内部类)只能访问外部的final类型或是隐式final类型的局部变量(就是值第一次被赋值了后面没被更改)

为了方便,JDK默认就为我们提供了专门写函数式的接口,这里只介绍Consumer

forEach循环

//第一种:类似于python
for (Integer i: list) {
            System.out.print(i+" ");
        }

//第二种:对象.foreach
list.forEach(i -> System.out.print(i+" "));

//第三种:用::更改源代码的accept()方法
list.forEach(System.out::print);
//该代码作用:输出list的all元素

枚举类

在类、接口之外的一种类型

假设现在我们想给小明添加一个状态(跑步、学习、睡觉),外部可以实时获取小明的状态:

public class Student {
    private final String name;
    private final int age;
    private String status;
  
  	//...
  
  	public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }
}

但是这样会出现一个问题,如果我们仅仅是存储字符串,似乎外部可以不按照我们规则,传入一些其他的字符串。这显然是不够严谨的!

有没有一种办法,能够更好地去实现这样的状态标记呢?我们希望开发者拿到使用的就是我们定义好的状态,我们可以使用枚举类!

public enum Status {
    RUNNING, STUDY, SLEEP    //直接写每个状态的名字即可,分号可以不打,但是推荐打上
}

使用枚举类也非常方便,我们只需要直接访问即可

public class Student {
    private final String name;
    private final int age;
    private Status status;
  
 		//...
  
  	public void setStatus(Status status) {   //不再是String,而是我们指定的枚举类型
        this.status = status;
    }

    public Status getStatus() {
        return status;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus());
}

枚举类型使用起来就非常方便了,其实枚举类型的本质就是一个普通的类,但是它继承自Enum类,我们定义的每一个状态其实就是一个public static final的Status类型成员变量!

// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
  public static final com.test.Status RUNNING;
  public static final com.test.Status STUDY;
  public static final com.test.Status SLEEP;
  public static com.test.Status[] values();
  public static com.test.Status valueOf(java.lang.String);
  static {};
}

既然枚举类型是普通的类,那么我们也可以给枚举类型添加独有的成员方法

public enum Status {
    RUNNING("睡觉"), STUDY("学习"), SLEEP("睡觉");   //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法!)

    private final String name;    //枚举的成员变量
    Status(String name){    //覆盖原有构造方法(默认private,只能内部使用!)
        this.name = name;
    }
  
  	public String getName() {   //获取封装的成员变量
        return name;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus().getName());
}

枚举类还自带一些继承下来的实用方法

Status.valueOf("")   //将名称相同的字符串转换为枚举
Status.values()   //快速获取所有的枚举

基本类型包装类

Java并不是纯面向对象的语言,虽然Java语言是一个面向对象的语言,但是Java中的基本数据类型却不是面向对象的。在学习泛型和集合之前,基本类型的包装类是一定要讲解的内容!

我们的基本类型,如果想通过对象的形式去使用他们,Java提供的基本类型包装类,使得Java能够更好的体现面向对象的思想,同时也使得基本类型能够支持对象操作!

image

  • byte -> Byte
  • boolean -> Boolean
  • short -> Short
  • char -> Character
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

包装类实际上就行将我们的基本数据类型,封装成一个类(运用了封装的思想)

private final int value;   //Integer内部其实本质还是存了一个基本类型的数据,但是我们不能直接操作

public Integer(int value) {
    this.value = value;
}

现在我们操作的就是Integer对象而不是一个int基本类型了!

public static void main(String[] args) {
     Integer i = 1;   //包装类型可以直接接收对应类型的数据,并变为一个对象!
     System.out.println(i + i);    //包装类型可以直接被当做一个基本类型进行操作!
}

自动装箱和拆箱

自动装箱:在对一个Integer类型的对象赋值时,叫自动装箱

自动拆箱:对一个Integer类型的对象做运算、赋值给别的变量时,叫拆箱

自动装箱原理:

Integer i = 1;    //其实这里只是简写了而已
Integer i = Integer.valueOf(1);  //编译后真正的样子

Integer.valueOf (x)原理:调用valueOf来生成一个Integer对象!

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)   //注意,Java为了优化,有一个缓存机制,如果是在-128~127之间的数,会直接使用已经缓存好的对象,而不是再去创建新的!(面试常考)!!!
       return IntegerCache.cache[i + (-IntegerCache.low)];
  	return new Integer(i);   //返回一个新创建好的对象
}

自动拆箱原理:

public static void main(String[] args) {
    Integer i = Integer.valueOf(1);
    int a = i;    //简写
    int a = i.intValue();   //编译后实际的代码
  
  	long c = i.longValue();   //其他类型也有!
}

==是指地址是否相同,equals()是指值是否相同。

当Integer的对象值在-128—127时,缓存机制(IntegerCache)使用缓存好的对象作为该Integer对象(此时无论多少个Integer对象,使用的都是同一个对象)

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1 == i2);   //实际上判断是两个对象是否为同一个对象(内存地址是否相同)【当i1,i2在-128—127间,则它们使用的是同一个对象】
    System.out.println(i1.equals(i2));   //这个才是真正的值判断!
}

思考:下面这种情况结果会是什么?True

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1+1 == i2+1);	//由于Integer对象经过了自动拆箱,所以等号两边都是基本数据int
}

Java异常机制

异常

比如数组越界异常,空指针异常,算术异常等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错)也可以提前声明,告知使用者需要处理可能会出现的异常!

运行时异常

定义:在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常。

特点:所有的运行时异常都直接继承自RuntimeException(RuntimeException也是继承Exception)

编译时异常

定义:在编译阶段就需要进行处理的异常(捕获异常)如果不进行处理,将无法通过编译!

特点:默认直接继承自Exception类的异常都是编译时异常

File file = new File("my.txt");
file.createNewFile();   //要调用此方法,首先需要处理异常

错误

错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能JVM就无法继续正常运行了

比如OutOfMemoryError就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)

int[] arr = new int[Integer.MAX_VALUE];   //能创建如此之大的数组吗?不能!

报错:

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at com.test.Main.main(Main.java:14)

错误都继承自Error类,一般情况下,程序中只能处理异常,错误是很难进行处理的,ErrorExecption都继承自Throwable类。当程序中出现错误或异常时又没有进行处理时,程序(当前线程)将终止运行:

int[] arr = new int[Integer.MAX_VALUE];
System.out.println("lbwnb");  //还能正常打印吗?

异常处理

程序出现异常时,默认会交给JVM来处理,JVM发现任何异常都会立即终止程序运行,并在控制台打印栈追踪信息。这是就需要编写程序手动捕获异常,使程序继续正常运行。(一旦手动捕获后,异常就不再交给JVM处理)

使用trycatch语句块来处理:

int[] arr = new int[5];
try{    //在try块中运行可能出现异常的代码
     arr[5] = 1;    //当代码出现异常时,异常会被捕获,并匹配catch块中捕获异常的类型,从而得到异常类型的对象
}catch (ArrayIndexOutOfBoundsException e){   //捕获的异常类型,并匹配catch块中捕获异常的类型,从而得到异常类型的对象
     e.printStackTrace()	//打印栈追踪信息,定位异常出现位置及原因
     System.out.println("程序运行出现异常!");  //出现异常时执行
}finally {
  System.out.println("finally:lbwnb");   //无论是否出现异常,都会在最后执行
}

//后面的代码会正常运行
System.out.println("lbwnb");

运行结果 :

java.lang.ArrayIndexOutOfBoundsException: 5
	at com.test.Main.main(Main.java:7)    //Main类的第7行出现问题
程序运行出现异常!
finally:lbwnb
lbwnb

finally:

  • try语句块至少要配合catchfinally中的一个:
try {
    int a = 10;
    a /= 0;
}finally {  //不捕获异常,程序会终止,但在最后依然会执行下面的内容
    System.out.println("lbwnb"); 
}
  • 思考:trycatchfinally执行顺序:try——catch(有异常才执行)——finally
private static int test(int a){
  try{
    return a;
  }catch (Exception e){
    return 0;
  }finally {
    a =  a + 1;
  }
}

运行时异常在编译时可以不用捕获,但是编译时异常必须进行处理

注 :

  1. 可以捕获到类型不止是Exception的子类,只要是继承自Throwalbe的类,都能被捕获,也就是说,Error也能被捕获,但是不建议这样做,因为错误一般是虚拟机相关的问题,出现Error应该从问题的根源去解决。
  2. 异常层级图

image

异常的抛出

传入错误参数,则需要通过throw关键字手动抛出异常(抛出异常后,后面的代码不再执行),可以方法内try catch处理异常,若不想在方法内处理,则需要同时告知上一级方法执行出现了问题(在方法后加throws Exception),此时上一级就需要try catch来处理异常。

【若最上级main也在方法后加了throws Exception则就是交给了JVM处理该异常】

public static void main(String[] args) {
        try {
            test(1, 0);
        } catch (Exception e) {   //捕获方法中会出现的异常并处理 or 接收子方法中传来的异常并处理;
            e.printStackTrace();
            System.out.println("出现异常");
        }
    }

    private static int test(int a, int b) throws Exception {  //声明并接收下面程序中抛出的异常类型
        if(b == 0) throw new Exception("0不能做除数!");  //创建异常对象并抛出异常,给上面接收
        return a/b;  //抛出异常会终止代码运行
    }
  • 非运行时异常必须捕获处理(不处理的话编译通过不了)
  • 运行时异常不强求手动捕获处理(JVM会处理)

注:当异常捕获出现嵌套时,只会在最内层被捕获:

public static void main(String[] args){
        try{
            test(1, 0);
        }catch (Exception e){
            System.out.println("外层");
        }
    }

    private static int test(int a, int b){
        try{
            if(b == 0) throw new Exception("0不能做除数!");
        }catch (Exception e){
            System.out.println("内层");
            return 0;
        }
        return a/b;
    }


//结果:内层

自定义异常

第一步:写一个类继承Exception

public class MyException extends Exception {  //直接继承即可
    
}

public static void main(String[] args) throws MyException {
        throw new MyException();   //直接使用
    }

第二步:使用父类的带描述的构造方法

public class MyException extends Exception {

    public MyException(String message){
        super(message);
    }
}

public static void main(String[] args) throws MyException {
    throw new MyException("出现了自定义的错误");
}

第三步:抛出自定义异常(可以用自定义异常的父类接收该异常)

try {
  throw new MyException("出现了自定义的错误");
} catch (Exception e) {    //捕获父异常类型
  System.out.println("捕获到异常");
}

多重异常捕获

多重异常捕获,类似于if-else if的结构,父异常类型只能放在最后!:

try {
  //....
} catch (NullPointerException e) {
            
} catch (IndexOutOfBoundsException e){

} catch (RuntimeException e){
            
}

try {
  //....
} catch (RuntimeException e){  //父类型在前,会将子类的也捕获

} catch (NullPointerException e) {   //永远都不会被捕获

} catch (IndexOutOfBoundsException e){   //永远都不会被捕获

}

多种异常一并处理用|

try {
     //....
} catch (NullPointerException | IndexOutOfBoundsException e) {  //用|隔开每种类型即可

}

泛型

泛型本质上也是一个语法糖(并不是JVM所支持的语法,编译后会转成编译器支持的语法,比如之前的foreach就是)。

类型擦除:在编译后会被擦除,变回上面的Object类型调用,但是类型转换由编译器帮我们完成,而不是我们自己进行转换(安全)

泛型使用细节:类上使用泛型,那 写在类名后面。方法上使用泛型,那 写在修饰符(static等)后面

举例:

为了统计学生成绩,要求设计一个Score对象,包括课程名称、课程号、课程成绩,但是成绩分为两种,一种是以优秀、良好、合格 来作为结果,还有一种就是 60.0、75.5、92.5 这样的数字分数,那么现在该如何去设计这样的一个Score类呢?现在的问题就是,成绩可能是String类型,也可能是Integer类型,如何才能很好的去存可能出现的两种类型呢?

一般方法:这种方法编译不会报错,但运行时会报错。所以不推荐!!!

//实体类
public class Score {
    String name;
    String id;
    Object score;  //因为Object是所有类型的父类,因此既可以存放Integer也能存放String

  	public Score(String name, String id, Object score) {
        this.name = name;
        this.id = id;
        this.score = score;
    }
}

//主方法
public static void main(String[] args) {
    Score score = new Score("数据结构与算法基础", "EP074512", "优秀");  //是String类型的
    Integer number = (Integer) score.score;  //获取成绩需要进行强制类型转换,虽然并不是一开始的类型,但是编译不会报错
}

//结果:运行时出现异常!
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at com.test.Main.main(Main.java:14)

范型方法:如果有错误编译就会报错,不用等运行项目时才发现报错。

//实体类
public class Score<T> {   //将Score转变为泛型类<T>
    String name;
    String id;
    T score;  //T为泛型,根据用户提供的类型自动变成对应类型

    public Score(String name, String id, T score) {   //提供的score类型即为T代表的类型
        this.name = name;
        this.id = id;
        this.score = score;
    }
}

//主方法
public static void main(String[] args) {
    //在调用实体类并输入数据的时候就可以确定T的类型,后面一个砖石运算符不用写String
    Score<String> score = new Score<String>("数据结构与算法基础", "EP074512", "优秀");
    Integer i = score.score;  //编译不通过,因为成员变量score类型被定为String!
}

原理:主方法在编译的时候被转换成如下代码(这就是泛型的类型擦除,T被擦除了,变成了Object)

//反编译后的代码
public static void main(String[] args) {
        Score score = new Score("数据结构与算法基础", "EP074512", "优秀");
        String i = (String)score.score;   //其实依然会变为强制类型转换,但是这是由编译器帮我们完成的
    }

泛型介绍

泛型只比普通的类多了一个类型参数,在使用的时候就需要指定具体的泛型类型。泛型名称一般取单个大写字母,如T代表Type的意思。也可以添加数字和其他字符,要设置多个泛型<T,V,M>

T如何被确定

①创建对象并添加数据时new Score<>(xxx,xxx,xxx),系统会根据输入的数据给泛型赋上相应的类型类。【也就是说只要在使用的时候可以通过传入的参数确定类型,都可以使用泛型】

②接收new Score的Score score 钻石运算符内的类型就需要自己根据可能输入的参数值来判断了。【若无法判断可以写成<?>,但最后需要使用该对象的参数的时候强转(因为此时参数类型为Object)。不推荐该方法】

泛型的使用范围:不可以使用在静态成员变量。(泛型是创建对象后编译器才能明确泛型类型,而静态类型是类具有的属性,编译器无法通过对象察觉泛型类型)

泛型类型:包括所有基本类型的包装类(Integer,Double,String),但不能使用基本数据类型(int,double等)

类的泛型方法

这种方法依附于类定义的泛型。原理是在创建该类对象的时候就可以确定T

举例:

public class Score<T>{
    String name;
    String id;
    T score;
    
public T getScore() {    //若方法的返回值类型为泛型,那么编译器会自动进行推断
  return score;
}

public void setScore(T score) {   //若方法的形式参数为泛型,那么实参只能是定义时的类型
  this.score = score;
}
}

自定义泛型方法

类的泛型方法需要依附于类,但自定义的泛型方法可以设置为在调用该方法传入参数的时候才确定类型E

静态方法的自定义泛型方法:

public static <E,ID> void test(E e,ID id){   //在方法定义前声明泛型
  System.out.println(e+id);
}

成员方法的自定义泛型方法:

public static <E> void test(E e){   //在方法定义前声明泛型
  System.out.println(e);
}

泛型的界限

上界

定义:

public class Score<T extends Number> { //设定泛型上界,必须是Number的子类

Score<? extends Number> score = new Score<> (xxx,xxxx,xxx); //只能接收Number及其子类的类型

范围:只有指定类型及指定类型的子类才可以作为其类型参数

(白话:输入(接收)的参数类型只能是指定类型及其指定类型的子类)

编译:在编译时类型擦除会把T类型的Score编译成指定的最高上界的类型

下界

定义:

下届只能在接收的时候设置,不能在实体类设置

Score<? super Integer> score = new Score<> (xxx,xxxx,xxx); //只能接收Integer及其父类的new Score类型

范围:只有指定类型或指定类型的父类才能作为类型参数

(白话:输入(接收)的参数类型只能是指定类型及其指定类型的父类)

编译:在编译时类型擦除会把T类型的Score编译成Object 类型(虽然设置了下界,但最高上界任然还是Object)

泛型与多态

接口

抽象类与接口类似就不介绍抽象类了

定义:

public interface ScoreInterface<T> {
    T getScore();
    void setScore(T t);
}

接口与实体类

//设置接口泛型与实体类泛型字母相同,创建实体类对象的时候就可以设置接口的类型了
public class Score<T> implements ScoreInterface<T>{   //将Score转变为泛型类<T>
//也可以在实现接口的时候就指定T    
//public class StringScore implements ScoreInterface<String>{   //在实现时明确类型
    private final String name;
    private final String id;
    private T score;

    public Score(String name, String id, T score) { 
        this.name = name;
        this.id = id;
        this.score = score;
    }

    public T getScore() {
        return score;
    }

    @Override
    public void setScore(T score) {
        this.score = score;
    }
}

接口注入bean后的调用

@Resource 
ScoreInterface<String> scoreinterface	//在注入接口bean对象的时候指定类型

思考:

既然继承后明确了泛型类型,那么为什么@Override不会出现错误呢,重写的条件是需要和父类的返回值类型、形式参数一致,而泛型默认的原始类型是Object类型,子类明确后变为Number类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?

答案:编译器在编译的时候生成了两个桥接方法用于支持重写(桥接:调用该类的成员方法,并将返回值返回至父类)

举例:

//父类
class A<T>{
    private T t;
    public T get(){
        return t;
    }
    public void set(T t){
        this.t=t;
    }
}

//子类
class B extends A<Number>{
    private Number n;

    @Override
    public Number get(){   //这并不满足重写的要求,因为只能重写父类同样返回值和参数的方法,但是这样却能够通过编译!
        return t;
    }

    @Override
    public void set(Number t){
        this.t=t;
    }
}

原理:编译时的代码。

class B extends A<Number>{
    private Number n;

    @Override
    public Number get(){  
        return t;
    }
    @Override
    public Object get(){
        return this.get();//调用返回Number的那个方法
    }

    @Override
    public void set(Number t){
        this.t=t;
    }
    @Override
    public void set(Object t ){
      this.set((Number)t ); //调用参数是Number的那个方法
    }
}

I/O流

  1. 所有流用完都需要close(); 也可以使用JDK1.7新增了try-with-resource语法会自动帮我们关闭流【注意,这种语法只支持实现了AutoCloseable接口的类!】
  2. 除文件流外的五个流只能嵌套文件流,他们之间无法相互嵌套,所以用流的时候需要判断应该什么流嵌套文件流
  3. 保存文件时如果没有该文件则会根据定义的路径自动创建

文件流

文件的相对路径是从与src相同的位置开始算起。如test.txt创建后就是与src文件同级

注:文件流不支持reset()方法

文件字节流

适用条件:所有文件字节流。

(因为每次获取文件内容都是通过一个字节获取,纯文本文件可能有中文等一字两字节的文本,需要同时获取两个字节才能得到一个字符)

输入流

读文件

举例:

public static void main(String[] args) {
    //test.txt:abcd
    try(FileInputStream inputStream = new FileInputStream("test.txt")) {
        byte[] bytes = new byte[inputStream.available()];   //我们可以提前准备好合适容量的byte数组来存放
        System.out.println(inputStream.read(bytes));   //一次性读取全部内容(返回值是读取的字节数)
        System.out.println(new String(bytes));   //通过String(byte[])构造方法得到字符串
    }catch (IOException e){
        e.printStackTrace();
    }
}

基本操作:

//查看该文件剩余可读的字节数量 【只有文件字节输入流有available,字符文件输入流没有】
byte[] bytes = new byte[inputStream.available()];	//用剩下可读的数量来定义数组大小

//一次行阅读bytes数组大小的字节,返回读取的内容。返回读到的字节数,没有可读的时候会返回-1  【可以通过】
inputStream.read(bytes);
//控制读取的范围:第二个参数是从给定数组的哪个位置开始放入内容,第三个参数是读取流中的字节数
inputStream.read(bytes, 1, 2);

//返回读取到的单个字符
(char)inputStream.read();

//跳过字节数1,并返回跳过的数量,即1
inputStream.skip(1);
输出流

写文件 (记得要在写完后outputStream.flush();刷新,以免写入失败

举例:

public static void main(String[] args) {
  //如果是追加写文件则需要调用使用这个构造方法
  //try(FileOutputStream outputStream = new FileOutputStream("output.txt", true))
    try(FileOutputStream outputStream = new FileOutputStream("output.txt")) {
        outputStream.write('c');   //同read一样,可以直接写入内容
      	outputStream.write("lbwnb".getBytes());   //也可以直接写入byte[]
      	outputStream.write("lbwnb".getBytes(), 0, 1);  //0是从第几个字节开始写,1是写的字节数量
      	outputStream.flush();  //建议在最后执行一次刷新操作(强制写入)来保证数据正确写入到硬盘文件中
    }catch (IOException e){
        e.printStackTrace();
    }
}

文件字符流(了解)

适用:纯文本 (因为只能够读取文字)

弊端:无available方法,数组长度需要自己定。只能按单个字符、数组指定长度读取

输入流

读文件

举例:

public static void main(String[] args) {
    try(FileReader reader = new FileReader("test.txt")){
        char[] str = new char[10];
        reader.read(str);
        System.out.println(str);   //直接读取到char[]中
    }catch (IOException e){
        e.printStackTrace();
    }
}

基本操作:

//跳过字符,返回跳过字符的数量
reader.skip(1);

//按单个字符读取,原本是返回字符数,但加了char就是返回读取到的单个字符
(char) reader.read();
    
//读取str数组长度的数据,并存入str数组中
reader.read(str);
输出流

写文件

举例:

注意FileWriter里有write和append方法,都是覆盖写入。只是append会返回一个看不懂的地址。 append支持链式调用:

writer.append("000")
     .append("111")
     .append("222");
public static void main(String[] args) {
    try(FileWriter writer = new FileWriter("output.txt")){
      	writer.getEncoding();   //支持获取编码(不同的文本文件可能会有不同的编码类型)
       writer.write('牛');
       writer.append('牛');   //其实功能和write一样
      	writer.flush();   //刷新
    }catch (IOException e){
        e.printStackTrace();
    }
}

File类

File类专门用于表示一个文件或文件夹,只不过它只是代表这个文件,但并不是这个文件本身。通过File对象,可以更好地管理和操作硬盘上的文件。

注意:

File file = new File(".idea/aa/bb/ccc");
file.mkdir();	//根据创建对象时的路径,创造一个文件夹。若路径中.idea、aa、bb中有一个不存在则不会创建
file.mkdirs();	//(强制创建文件夹)有以上路径则创建文件夹cc,没有这就创建全部路径知道创建出cc文件夹

基本操作:

public static void main(String[] args) {
    File file = new File("test.txt");   //直接创建文件对象,可以是相对路径,也可以是绝对路径
    System.out.println(file.exists());   //此文件是否存在
    System.out.println(file.length());   //获取文件的大小
    System.out.println(file.isDirectory());   //是否为一个文件夹
    System.out.println(file.canRead());   //是否可读
    System.out.println(file.canWrite());   //是否可写
    System.out.println(file.canExecute());   //是否可执行
    
    System.out.println(Arrays.toString(file.list()));   //快速获取文件夹下的文件名称列表
	for (File f : file.listFiles()){   //获取所有file下的子文件的File对象f
    	System.out.println(f.getAbsolutePath());   //获取文件的绝对路径
}

文件流练习

拷贝文件夹下的所有文件到另一个文件夹

推荐参考答案,因为自己写用了available,这个在网络传输的时候不准

自己写:

public static void main(String[] args) {
        int i=0;
        //输入流文件夹
        File file01 = new File(".idea");
        //输出流文件夹 (注意:这里是复制一个file01文件夹到该文件夹下,所以路径最后要是/
        File file02 = new File("test/cc");
        file02.mkdirs();

        //循环获取file01文件下的每个文件的对象,然后依次复制到另一个文件下
        // !!!注意这里一定要是先循环文件在开流,不能先开流在循环文件。因为反过来文件资源不会立即关闭浪费资源
       for(File file : file01.listFiles()){
           try(FileInputStream inputStream = new FileInputStream(file);
               FileOutputStream outputStream = new FileOutputStream(file02.getPath()+"/"+file.getName())) {
                byte [] arr = new byte[inputStream.available()];
                inputStream.read(arr);
                outputStream.write(arr);
                outputStream.flush();
               System.out.println("复制了"+i+"个文件");
               i++;
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }

参考答案:

//参考答案:
 public static void main(String[] args) {
        //输入流文件夹
        File file01 = new File(".idea");
        //输出流文件夹 (注意:这里是复制一个file01文件夹到该文件夹下,所以路径最后要是/
        File file02 = new File("test/cc");
        file02.mkdirs();

        //循环获取file01文件下的每个文件的对象,然后依次复制到另一个文件下
        // !!!注意这里一定要是先循环文件在开流,不能先开流在循环文件。因为反过来文件资源不会立即关闭浪费资源
       for(File file : file01.listFiles()){
           try(FileInputStream inputStream = new FileInputStream(file);
               FileOutputStream outputStream = new FileOutputStream(file02.getPath()+"/"+file.getName())) { //复制到的新文件写法要注意
                byte [] arr = new byte[20];
                int temp;
                //判断有没有复制到最后,到了最后temp=-1,否则temp是等于arr读到的字节
                while ((temp=inputStream.read(arr))!=-1){
                    outputStream.write(arr,0,temp);
                }
                //别忘了刷新
                outputStream.flush();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }

转换流

优化的对象是:文件字节流

原理【装饰者模式】:转换流也是继承了文件流的基础上进行了额外的操作,所以操作I/O文件的还是文件流(在文件字节流的基础上套转换流,从而获得文件字符流的便利。)

拥有更多种文件流的操作,所以操作文件流时都会嵌套转换流便于操作

便利:

  • 不用写入一个字符串再去转byte类型
  • 可以读取文件字节流,按字符的方式读取
  • 可以适用reset()、mark()【具体看缓冲流】

举例:

OutputStreamWriter嵌套FileOutputStream

public static void main(String[] args) {
     //嵌套:
        try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))){  //虽然给定的是FileOutputStream,但是现在支持以Writer的方式进行写入
            writer.write("lbwnb");   //以操作Writer的样子写入OutputStream
            writer.append("123");   //这里的append是追加写
        }catch (IOException e){
            e.printStackTrace();
        }
    }

InputStreamReader嵌套FileInputStream

public static void main(String[] args) {
    try(InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"))){  //虽然给定的是FileInputStream,但是现在支持以Reader的方式进行读取
        System.out.println((char) reader.read());
    }catch (IOException e){
        e.printStackTrace();
    }
}

缓冲流

缓存流能够提高文件流读取和写入的速度 以及重新读取之前的数据,所以一般操作文件都会在套完转换流后再套一层缓冲流。

原理【装饰者模式】:继承了文件流的继承进行了缓存操作,所以操作I/O文件的还是文件流

使文件流在缓冲区操作,提前把 文件的内容、需要输出到文件的内容 放到缓冲流中,便于后续快速获取。只使用了缓冲流,方法是和文件流一样的,只多了reset()、maek()方法

image

缓冲字节流

输入流BufferedInputStream的嵌套:

BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"))

输出流BufferedOutputStream的嵌套:

BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"))

缓冲字符流(了解)

输出流BufferedReader的嵌套:

BufferedReader reader = new BufferedReader(new FileReader("test.txt"))

输入流BufferedWriter的嵌套:

BufferedWriter writer= new BufferedWriter(new FileWriter("output.txt"))

比文件字符流多的操作:

//按行读取,返回读取的当行内容
reader.readLine();

//读取多行内容,还可以将多行依次转换为集合类提到的Stream流
reader
                .lines()
                .limit(2)
                .distinct()
                .sorted()
                .forEach(System.out::println);

//换行
writer.newLine();

reset()、mark()方法

缓存机制的关键!可以重新读取之前读过的数据

白话:mark就像书签一样,在这个BufferedReader对应的buffer里作个标记,以后再调用reset时就可以再回到这个mark过的地方。mark方法有个参数,通过这个整型参数(reallimit),你告诉系统,希望在读出这么多个字符之前,这个mark保持有效。读过这么多字符之后,系统可以使mark不再有效,而你不能觉得奇怪或怪罪它。这跟buffer有关,如果你需要很长的距离,那么系统就必须分配很大的buffer来保持你的mark。

但是实操会发现不是这样的,超过了reallimit 还是可以回到mark的地方,因为mark()后保存的读取内容是取readlimit和BufferedInputStream类的缓冲区大小两者中的最大值,而并非完全由readlimit确定。因此我们需要限制一下缓冲区大小和reallimit一致

缓冲区默认大小:8192

public static void main(String[] args) {
    try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"), 1)){  //将缓冲区大小设置为1
        bufferedInputStream.mark(1);   //只保留之后的1个字符
        System.out.println((char) bufferedInputStream.read());
        System.out.println((char) bufferedInputStream.read());   //已经超过了readlimit,继续读取会导致mark失效
        bufferedInputStream.reset();   //mark已经失效,无法reset()
        System.out.println((char) bufferedInputStream.read());
        System.out.println((char) bufferedInputStream.read());
    }catch (IOException e) {
        e.printStackTrace();
    }
}

打印流(了解)

打印流其实我们从一开始就在使用了,比如System.out就是一个PrintStream,PrintStream也继承自FilterOutputStream类因此依然是装饰我们传入的输出流,但是它存在自动刷新机制,例如当向PrintStream流中写入一个字节数组后自动调用flush()方法。PrintStream也永远不会抛出异常,而是使用内部检查机制checkError()方法进行错误检查。最方便的是,它能够格式化任意的类型,将它们以字符串的形式写入到输出流。

public final static PrintStream out = null;

可以看到System.out也是PrintStream,不过默认是向控制台打印,我们也可以让它向文件中打印:

public static void main(String[] args) {
    try(PrintStream stream = new PrintStream(new FileOutputStream("test.txt"))){
        stream.println("lbwnb");   //其实System.out就是一个PrintStream
    }catch (IOException e){
        e.printStackTrace();
    }
}

我们平时使用的println方法就是PrintStream中的方法,它会直接打印基本数据类型或是调用对象的toString()方法得到一个字符串,并将字符串转换为字符,放入缓冲区再经过转换流输出到给定的输出流上。

image

因此实际上内部还包含这两个内容:

/**
 * Track both the text- and character-output streams, so that their buffers
 * can be flushed without flushing the entire stream.
 */
private BufferedWriter textOut;
private OutputStreamWriter charOut;

与此相同的还有一个PrintWriter,不过他们的功能基本一致,PrintWriter的构造方法可以接受一个Writer作为参数,这里就不再做过多阐述了。

数据流(了解)

用于保存(写入)数据

注意:写入的是二进制数据,并不是写入的字符串,使用DataInputStream可以读取,一般他们是配合一起使用的。

数据流DataInputStream也是FilterInputStream的子类,同样采用装饰者模式,最大的不同是它支持基本数据类型的直接读取:

public static void main(String[] args) {
    try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream("test.txt"))){
        System.out.println(dataInputStream.readBoolean());   //直接将数据读取为任意基本数据类型
    }catch (IOException e) {
        e.printStackTrace();
    }
}

用于写入基本数据类型:

public static void main(String[] args) {
    try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("output.txt"))){
        dataOutputStream.writeBoolean(false);
    }catch (IOException e) {
        e.printStackTrace();
    }
}

对象流

用于保存(写入)数据,功能比数据流强大,不仅能保存基本数据类型,还能保存对象。【需要保存的对象实体类一定要实现接口序列化!!!】

ObjectOutputStream不仅支持基本数据类型,通过对对象的序列化操作,以某种格式保存对象,来支持对象类型的IO,注意:它不是继承自FilterInputStream的。【在存对象和读对象的时候,要保证实体类不能有修改,一旦修改,实体类版本号会不同,就无法读取之前存储的实体类对象,需要删除原保存的内容重新写入】

public static void main(String[] args) {
    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt"));
         ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))){
        People people = new People("lbw");
        outputStream.writeObject(people);
      	outputStream.flush();
        people = (People) inputStream.readObject();
        System.out.println(people.name);
    }catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

static class People implements Serializable{   //必须实现Serializable接口才能被序列化
    String name;

    public People(String name){
        this.name = name;
    }
}

六大流,十六小流总结

情况:

  1. 需要简化操作的时候,且保存的内容肉眼和识别没有加密时【转换流】:

    InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"));
    OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))
  2. 需要保存实体类对象、基本类型数据时【使用对象流(序列化流)】,保存内容加密了,只能对象流才能读取:

    ObjectOutputStream writer = new ObjectOutputStream(new FileOutputStream("output.txt");
    ObjectInputStream reader = new ObjectInputStream(new FileInputStream("output.txt"));
  3. 需要加快读取和保存文件的时间,保存内容可识别无加密【缓冲流】:

    BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"));
    BufferedInputStream reader = new BufferedInputStream(new FileInputStream("test.txt"));

Java多线程

线程基本操作是对线程的中断、加入等操作。线程锁的操作是对设置锁的代码块的等待、唤醒等操作。

介绍:进程是程序执行的实体,每一个进程都是一个应用程序(比如我们运行QQ、浏览器、LOL、网易云音乐等软件),都有自己的内存空间,CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮转调度算法,来实现多个进程的同时运行。

image

进程想要同时执行很麻烦,因为内存空间不同导致数据交换十分困难。于是,线程横空出世,一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),上下文切换速度也高于进程【一个进程可以有多个线程】

image

线程的基本操作

在没创建线程直接使用Thread里的方法,操作对象是main方法这个线程

线程创建和启动

  1. 创建线程构造方法中只有一个run方法,所以可以用lamda表达式
  2. 启动线程有start()和run()方法,前者才是多线程启动,后者是单线程启动
public static void main(String[] args) {
    //创建线程
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是一号线程:"+i);
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是二号线程:"+i);
        }
    });
    //启动线程
    t1.start();
    t2.start();
}

注意:我们发现还有一个run方法,也能执行线程里面定义的内容,但是run是直接在当前线程执行,并不是创建一个线程执行!

image

线程休眠和中断

获取当前线程的对象:

Thread t = new Thread(() -> {
     Thread me = Thread.currentThread();	//获取当前线程的对象
     String me = Thread.currentThread().getName();	//获取当前线程的名字
 }
 t.setName("对象名");  	//在线程外设置对象名
 t.start();		//启动线程后,Thread.currentThread().getName()读到的就是设置的对象名。没有定义名字就是系统给的名字

休眠:【类方法】

Thread.sleep(xxxx) //单位是毫秒,让当前进程休眠x秒

中断:【对象方法】

  • stop() 方法(不建议,因为强制性太强会导致资源释放不充分)
Thread me = Thread.currentThread();   //获取当前线程对象
me.stop();  //此方法会直接终止此线程
  • interrupt() 方法 (如果是要对线程中断用该方法,要对代码块中断就用后面的wait() )

    原理:在线程内写获取中断信号的代码,线程外(如main线程)写该线程对象的中断信号。main和该线程同时运行,直到main执行中断信号,该线程获取到就会执行if里的代码(这里可以return中断,也可以做别的操作)

public static void main(String[] args) throw InterruptedException{
    Thread t = new Thread(() -> {
        System.out.println("线程开始运行!");
        while (true){	//while是细节,如果没有while,t线程执行完了,main线程都没有把信号释放出来
            if(Thread.currentThread().isInterrupted()){   //判断是否存在中断标志
                System.out.println("发现中断信号,复位,继续运行...");
                Thread.interrupted();  //复位中断标记(返回值是当前是否有中断标记,这里不用管)
            }
        }
    });
    t.start();
    Thread.sleep(3000);   //休眠3秒
    t.interrupt();   //调用t的interrupt方法
}
  • suspend()、resume() (不建议,容易死锁)

    不推荐使用 suspend() 去挂起线程的原因,是因为suspend()在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("线程开始运行!");
        Thread.currentThread().suspend();   //暂停此线程
        System.out.println("线程继续运行!");
    });
    t.start();
    try {
        Thread.sleep(3000);   //休眠3秒,一定比线程t先醒来
        t.resume();   //恢复此线程
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

线程优先级、礼让和加入

线程优先级:

Java程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源!【优先级越高的线程,获得CPU资源的概率会越大,并不是说一定优先级越高的线程越先执行!】

  • MIN_PRIORITY 最低优先级
  • MAX_PRIORITY 最高优先级
  • NOM_PRIORITY 常规优先级
public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("线程开始运行!");
    });
    t.start();
    t.setPriority(Thread.MIN_PRIORITY);  //通过使用setPriority方法来设定优先级
}

线程礼让:(了解)【类方法】

执行到礼让后,会让别的同优先级线程先执行一小步

Thread.yield();	//线程内执行

线程加入:【对象方法】

两个线程同时启动,一个线程执行到join会等待另一个线程执行完成后再继续进行。不是将另一个线程和当前线程合并!

Thread t1 = new Thread(() -> {
    
})
Thread t2 = new Thread(() -> {
	t1.join	//此时t2线程停下来,等t1执行完再执行
})
t1.start();
t2.start();

线程锁和线程同步

在开始讲解线程同步之前,我们需要先了解一下多线程情况下Java的内存管理:

image

线程之间的共享变量(比如之前悬念中的value变量)存储在主内存(main memory)中,每个线程都有一个私有的工作内存(本地内存),工作内存中存储了该线程以读/写共享变量的副本。它类似于我们在计算机组成原理中学习的多处理器高速缓存机制:

image

高速缓存通过保存内存中数据的副本来提供更加快速的数据访问,但是如果多个处理器的运算任务都涉及同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入高速缓存引发的新问题,称之为:缓存一致性

实际上,Java的内存模型也是这样类似设计的,当我们同时去操作一个共享变量时,如果仅仅是读取还好,但是如果同时写入内容,就会出现问题!好比说一个银行,如果我和我的朋友同时在银行取我账户里面的钱,难道取1000还可能吐2000出来吗?我们需要一种更加安全的机制来维持秩序,保证数据的安全性

思考:该代码运行结果

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

结果:不一定是20000,原因就是缓存一致性。当两个线程同时读取value的时候,可能会同时拿到同样的值,而进行自增操作之后,也是同样的值,再写回主内存后,本来应该进行2次自增操作,实际上只执行了一次!【此时就需要引入线程锁来解决问题】

线程锁

原理:当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他的线程才能拿到这把锁并开始执行同步代码块里面的内容。(实际上synchronized是一种悲观锁,随时都认为有其他线程在对数据进行修改,后面有机会我们还会讲到乐观锁,如CAS算法)

synchronized(){}关键字创建线程锁。

  1. 它需要在括号中填入一个内容作为锁,必须是一个对象或是一个类。【被作为锁的对象或者类不会被影响】

    【别的线程和该线程操作同一个变量时,锁必须和该线程的锁一致,否则无用】

  2. {}写入可能和别的线程操作同一变量的代码。

  • 作用于代码块【麻烦,在不用等待和唤醒的时候不建议使用(等待和唤醒是必须用于代码块)】
private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){	//同一把锁
                value++;
            }
        }
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){	//同一把锁
                value++;
            }
        }
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}
  • 作用于方法【便捷,建议使用】

作用于方法,只要不同线程调用该方法,就是在相同的锁情况执行,和前面效果一样

​ 我们发现实际上效果是相同的,只不过这个锁不用你去给,如果是静态方法,就是使用的类锁,而如果是普通成员方法,就是使用的对象锁。

private static int value = 0;

private static synchronized void add(){
    value++;
}

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("线程1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("线程2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主线程停止1秒,保证两个线程执行完成
    System.out.println(value);
}

死锁

介绍:死锁的概念在操作系统中也有提及,它是指两个线程相互持有对方需要的锁,但是又迟迟不释放,导致程序卡住:

image

检测死锁的方法:

cmd中输入jps查看java进程,ps是电脑进程。jstack是自动找到死锁,并打印相关线程的栈追踪信息。

进程的等待与唤醒

wait()notify()以及notifyAll()方法需要配合synchronized关键字使用,并且只能在同步代码块(synchronized关键字的代码块)中才能使用。

wait()notify()以及notifyAll()作用:使上了同一把锁的进程可以轮流运行。【白话:一边代码块执行到一半,等另一边执行完代码块再继续执行未执行完的代码块】

举例:代码解释:对象的wait()方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常!

public static void main(String[] args) throws InterruptedException {
    Object o1 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o1){
            try {
                System.out.println("开始等待");
                o1.wait();     //进入等待状态并释放锁给别的线程
                System.out.println("等待结束!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o1){
            System.out.println("开始唤醒!");
            o1.notify();     //唤醒处于随机一个等待状态的线程,但仍然要执行完当前锁的代码块
//            o1.notifyall();	//唤醒all处于等待的线程,但仍然要执行完当前锁的代码块
          	for (int i = 0; i < 50; i++) {
               	System.out.println(i);   
            }
          	//唤醒后依然需要等待这里的锁释放之前等待的线程才能继续
        }
    });
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

ThreadLocal

用于存储一个线程专有的值【对象方法】

ThreadLocal类,来创建工作内存中的变量,它将我们的变量值存储在内部(只能存储一个变量),不同的变量访问到ThreadLocal对象时,都只能获取到自己线程所属的变量。【每个线程的工作内存空间不同,所以线程之间相互独立,互不相关】

public static void main(String[] args) throws InterruptedException {
    ThreadLocal<String> local = new ThreadLocal<>();  //注意这是一个泛型类,存储类型为我们要存放的变量类型
    Thread t1 = new Thread(() -> {
        local.set("lbwnb");   //将变量的值给予ThreadLocal
        System.out.println("线程1变量值已设定!");
        try {
            Thread.sleep(2000);    //间隔2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1读取变量值:");
        System.out.println(local.get());   //尝试获取ThreadLocal中存放的变量
    });
    Thread t2 = new Thread(() -> {
        local.set("yyds");   //将变量的值给予ThreadLocal
        System.out.println("线程2变量值已设定!");
    });
    t1.start();
    Thread.sleep(1000);    //间隔1秒
    t2.start();
}

//结果:lbwnb。就算t2也设置了值,但不影响t1的值

拓展:子类线程也获得不了父类线程设置的值,但可以通过用InheritableThreadLocal方法来解决这个问题。(在InheritableThreadLocal存放的内容,会自动向子线程传递)

public static void main(String[] args) {
    ThreadLocal<String> local = new InheritableThreadLocal<>();
    Thread t = new Thread(() -> {
       local.set("lbwnb");
        new Thread(() -> {
            System.out.println(local.get());
        }).start();
    });
    t.start();
}

线程小结及练习

  1. 一般都是将锁设在方法里,线程来调用方法。
  2. 尽量不在锁里睡眠sleep
private static List<Object> list =new ArrayList<>();

    public static void main(String[] args) {
        Thread chef01 = new Thread(Main::cook);
        chef01.setName("厨师一");
        Thread chef02 = new Thread(Main::cook);
        chef02.setName("厨师二");
        chef01.start();
        chef02.start();

        Thread con01 = new Thread(Main::eat);
        con01.setName("消费者一");
        Thread con02 = new Thread(Main::eat);
        con02.setName("消费者二");
        Thread con03 = new Thread(Main::eat);
        con03.setName("消费者三");
        con01.start();
        con02.start();
        con03.start();
    }

    private static void cook() {
        while (true){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (list){
                list.add(new Object());
                System.out.println(new Date() +Thread.currentThread().getName()+": 添加了新菜!");
                list.notifyAll();
            }
        }
    }

    private static void eat() {
        while (true){
            try {
                synchronized (list){
                    while (list.isEmpty())list.wait();    //当只做了一盘菜,但有多个顾客要时,先抢到的就出菜,没抢到的就继续循环等待
                    System.out.println(new Date()+Thread.currentThread().getName()+" 拿走了一盘菜!");
                    list.remove(0);
                }
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

定时器

运行的时候会发现,如果不手动结束任务,在执行完任务后不会终止程序。这是因为Timer内存维护了一个任务队列和一个工作线程。(详细的去看源码)

public static void main(String[] args) {
    Timer timer = new Timer();    //创建定时器对象
    timer.schedule(new TimerTask() {   //注意这个是一个抽象类,不是接口,无法使用lambda表达式简化,只能使用匿名内部类
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());    //打印当前线程名称
            timer.cancel(); 	//结束任务
        }
    }, 1000);    //执行一个延时任务
}

守护线程

白话:守护线程是在别的线程结束后会自动结束自己的线程,无论运行到哪里都会立即结束。

守护线程里的子线程也会因父类是守护线程,其他线程都结束后,该父类与子类也会自动结束。

不要把守护进程和守护线程相提并论!守护进程在后台运行运行,不需要和用户交互,本质和普通进程类似。而守护线程就不一样了,当其他所有的非守护线程结束之后,守护线程是自动结束,也就是说,Java中所有的线程都执行完毕后,守护线程自动结束,因此守护线程不适合进行IO操作,只适合打打杂

在守护线程中产生的新线程也是守护的:【仍然会被中断】

public static void main(String[] args) throws InterruptedException{
    Thread t = new Thread(() -> {
        Thread it = new Thread(() -> {
            while (true){
                try {
                    System.out.println("程序正常运行中...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        it.start();
    });
    t.setDaemon(true);   //设置为守护线程(必须在开始之前,中途是不允许转换的)
    t.start();
    for (int i = 0; i < 5; i++) {
        Thread.sleep(1000);
    }
}

集合类并行流

集合类中通过并行流可以大大提高运算速度。注意:打印的时候使用并行流中的foreach就不会按照原来的顺序打印,所以需要使用并行流中的forEachOrdered打印就可以保持顺序的单线程打印了。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, 5, 2, 9, 3, 6, 0));
    list
            .parallelStream()    //获得并行流
            .forEachOrdered(System.out::println);
}

Arrays数组工具类中也有并行方法:

public static void main(String[] args) {
    int[] arr = new int[]{1, 4, 5, 2, 9, 3, 6, 0};
    Arrays.parallelSort(arr);   //使用多线程进行并行排序,效率更高
    System.out.println(Arrays.toString(arr));
}

数据结构

线性表

顺序表

用数组实现一个表,对其增加、删除后数据对自动填补

image

//抽象类
/**
 * 线性表抽象类
 * @param <E> 存储的元素(Element)类型
 */
public abstract class AbstractList<E> {
    /**
     * 获取表的长度
     * @return 顺序表的长度
     */
    public abstract int size();

    /**
     * 添加一个元素
     * @param e 元素
     * @param index 要添加的位置(索引)
     */
    public abstract void add(E e, int index);

    /**
     * 移除指定位置的元素
     * @param index 位置
     * @return 移除的元素
     */
    public abstract E remove(int index);

    /**
     * 获取指定位置的元素
     * @param index 位置
     * @return 元素
     */
    public abstract E get(int index);
}


//具体实现类
public class MyList<E> extends AbstractList{
    private int size=0;
    private Object[] arr = new Object[1];


    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(Object o, int index) {
        //判断插入下标是否有误
        if (index > size) throw new IllegalArgumentException("非法位置插入");
        //满了就扩容
        if (size == this.arr.length){
            Object[] arr = new Object[this.arr.length+10];
            for (int i = 0; i < this.arr.length; i++) arr[i]=this.arr[i];
            this.arr=arr;
        }
        //后移
        for (int i = size-1; i >= index; i--) {
            arr[i+1]=arr[i];
        }
        //插入
        arr[index]=o;
        size++;
    }

    @Override
    public Object remove(int index) {
        //判断删除下标是否有误
        if (index >= size) throw new IllegalArgumentException("非法位置输入");
        //记录删除的数据
        E e = (E) arr[index];
        //前移(这里有个细节,不需要删除再前移而是直接覆盖即可)
        for (int i = index; i < size-1; i++) {
            arr[i]=arr[i+1];
        }
        size--;
        return e;
    }

    @Override
    public Object get(int index) {
        //判断获取元素的下标是否有误
        if (index >= size) throw new IllegalArgumentException("非法位置输入!");
        return arr[index];
    }
}

链表

官方:数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构

白话:就是每一个结点存放一个元素和一个指向下一个结点的引用(C语言里面是指针,Java中就是对象的引用,代表下一个结点对象)

image

//抽象类和上面一样

//具体实现类
public class LinkedList<E> extends AbstractList<E>{
    private int size=0;
    private Node<E> head = new Node<E>(null);

    private static class Node<E>{
        private E e;
        private Node<E> next;
        public Node(E e){
            this.e=e;
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(E o, int index) {
        //下标超出
        if (index > size) throw new IllegalArgumentException("非法下标输入");
        //循环找到需要插入的前一个node
        Node<E> node=head,temp;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        //暂存index后一个的结点指针
        temp=node.next;
        //创建一个新的结点插入到index上
        node.next=new Node<>(o);
        node.next.next=temp;
        size++;
    }

    @Override
    public E remove(int index) {
        //非法下标输入
        if (index >= size) throw new IllegalArgumentException("非法下标输入");
        //循环找到需要删除的结点的前一个结点
        Node<E> node = head,temp;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        //暂存删除的节点
        temp=node.next;
        //改变index前一个结点的next,直接跨过index,与下一个节点关联
        node.next=node.next.next;
        size--;
        return temp.e;
    }

    @Override
    public E get(int index) {
        //检查下标合法性
        if (index >= size )throw new IllegalArgumentException("非法下标输入");
        //循环找到index位置上的结点值并return
        Node<E> node=head;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        return node.next.e;
    }
}

顺序表优缺点:

  • 访问速度快,随机访问性能高
  • 插入和删除的效率低下,极端情况下需要变更整个表
  • 不易扩充,需要复制并重新创建数组

链表优缺点:

  • 插入和删除效率高,只需要改变连接点的指向即可
  • 动态扩充容量,无需担心容量问题
  • 访问元素需要依次寻找,随机访问元素效率低下

JVM调用方法的时候就是一个栈操作

先入后出原则。类似于杯子只有一端开口。入栈(压栈)、出栈

image

代码实现:可以看作是单指针法,用size做指针

//抽象类
/**
 * 抽象类型栈,待实现
 * @param <E> 元素类型
 */
public abstract class AbstractStack<E> {

    /**
     * 出栈操作
     * @return 栈顶元素
     */
    public abstract E pop();

    /**
     * 入栈操作
     * @param e 元素
     */
    public abstract void push(E e);
}

//具体实现类
public class ArrayStack<E> extends AbstractStack<E>{
    private int size=0;
    private Object[] arr = new Object[1];


    @Override
    public E pop() {
        return (E) arr[size-1];
    }

    @Override
    public void push(E e) {
        //满了就扩容
        if (size == this.arr.length){
            Object[] arr = new Object[this.arr.length+10];
            for (int i = 0; i < this.arr.length; i++) arr[i]=this.arr[i];
            this.arr=arr;
        }
        //压栈
        arr[size++]=e;
    }
}

队列

和食堂排队一样,先进先出

image

代码实现:可以看作是双指针法,head、tail,分别确定入队和出队元素的下标

//抽象类
/**
 *
 * @param <E>
 */
public abstract class AbstractQueue<E> {

    /**
     * 进队操作
     * @param e 元素
     */
    public abstract void offer(E e);

    /**
     * 出队操作
     * @return 元素
     */
    public abstract E poll();
}

//具体实现类

public class ArrayQueue<E> extends AbstractQueue<E> {   //队列:编写代码可以理解为一个循环圈
    private Object[] arr=new Object[4];
    private int head=0,tail=0;
    
    @Override
    public void offer(E e) {
        //保证下一个不是head就可以继续插进去
        int next=(tail+1)% arr.length;
        if (next==head) return;
        //插入并循环
        arr[tail]=e;
        tail=(tail+1)% arr.length;
    }

    @Override
    public E poll() {
        E e= (E) arr[head];
        head=(head+1)%arr.length;
        return e;
    }
}

链表实现栈:压栈头插法,出栈头取法

压栈就加到链表的首节点,链表的数据后推一个单位;出栈就将首节点断出来,头节点与首节点的下一节点相连。

//把t元素压入栈(相当于链表的头插法)
    public void push(T t){
        //首结点指向的第一个元素
        Node first=head.next;
        Node newNode = new Node(t, null);
        //首结点指向新结点
        head.next=newNode;
        //新结点指向原来的第一节点first
        newNode.next=first;
        //元素个数加1
        N++;
    }
 
    //把元素弹出栈
    pub
        Node first=head.next;
        if(first==null){
            return null;
        }
        head.next=first.next;
        //元素个数-1
        N--;
        return first.data;
    }

链表实现队列:入队则尾插法,出队则头取法

public void push(int val){
        Node node = new Node(0);
        tail.val = val;
        tail.next = node;
        tail = tail.next;
        size++;
    }

    public int pop(){
        if(size == 0) return -1;
        int a = head.next.val;
        head = head.next;
        size--;
        System.out.println(a);
        return a;
    }

二叉树

特点:一对多。(顺序表、链表是一对一)

位于最顶端的结点(没有父结点)我们称为根结点,而结点拥有的子节点数量称为,每向下一级称为一个层次,树中出现的最大层次称为树的深度(高度)

image

二叉树

二叉树每个结点最多有两棵树,即左右子树

image

二叉树数学性质:

  • 在二叉树的第i层上最多有2^(i-1) 个节点。
  • 二叉树中如果深度为k,那么最多有2^k-1个节点。

二叉树代码实现:

public class TreeNode<E> {
    public E e;   //当前结点数据
    public TreeNode<E> left;   //左子树
    public TreeNode<E> right;   //右子树
}

二叉树的遍历方式:

  • 前序遍历:从二叉树的根结点出发,到达结点时就直接输出结点数据,按照先向左在向右的方向访问。ABCDEF

    public static void test(TreeNode root){
            //判断节点存不存在
            if (root == null ) return;
            //先序遍历,先输出根节点
            System.out.print(root.e+"  ");
            test(root.left);
            test(root.right);
        }
  • 中序遍历:从二叉树的根结点出发,优先输出左子树的节点的数据,再输出当前节点本身,最后才是右子树。CBDAEF

    public static void test(TreeNode root){
            //判断节点存不存在
            if (root == null ) return;
            test(root.left);
            //中序遍历,中间输出根节点
            System.out.print(root.e+"  ");
            test(root.right);
        }
  • 后序遍历:从二叉树的根结点出发,优先遍历其左子树,再遍历右子树,最后在输出当前节点本身。CDBFEA

    public static void test(TreeNode root){
            //判断节点存不存在
            if (root == null ) return;
            test(root.left);
            test(root.right);
            //中序遍历,中间输出根节点
            System.out.print(root.e+"  ");
        }
满二叉树与完全二叉树

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树(白话:所有叶子节点都在同一层)

完全二叉树:完全二叉树与满二叉树不同的地方在于,它的最下层叶子节点可以不满,但是最下层的叶子节点必须靠左排布

image

快速查询

哈希表

JDK1.8 后才有的。本质就是一个存放链表的数组

由于顺序表查询效率高,但插入删除效率低。然而链表又是插入删除效率高,查询效率慢。于是又了折中的哈希表

image

解释:数组中每个元素都是一个头节点,用于保存数据。通过hash算法,可以快速得到元素应该放置在数组的哪个下标位置。

//假设hash表长度为16,hash算法为:
private int hash(int hashcode){
  return hashcode % 16;
}

hash碰撞:某两个数通过某些hash算法(例如以上的算法)后得到的hash值相同。

此时先得到hash值得数先进该hash值的数组下标,后面的数就以链表的形式与前一个数连接,以此类推

二叉排序树

定义:每个节点的左子树的值小于该节点的值,每个节点的右子树的值大于该节点的值

image

平衡二叉树

定义:每个结点的左右两个子树的高度差的绝对值不超过1

如何保证二叉排序树是平衡二叉树:同时要求每个节点的左右子树都是平衡二叉树

image

左左失衡

image

右右失衡

image

左右失衡

image

右左失衡

通过以上四种情况的处理,最终得到维护平衡二叉树的算法。

红黑树

红黑树也是二叉排序树的一种改进,同平衡二叉树一样,红黑树也是一种维护平衡的二叉排序树,但是没有平衡二叉树那样严格(平衡二叉树每次插入新结点时,可能会出现大量的旋转,而红黑树保证不超过三次),红黑树降低了对于旋转的要求,因此效率有一定的提升同时实现起来也更加简单。但是红黑树的效率却高于平衡二叉树,红黑树也是JDK1.8中使用的数据结构!

image

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点的两边也需要表示(虽然没有,但是null也需要表示出来)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

我们来看看一个节点,是如何插入到红黑树中的:

基本的 插入规则和平衡二叉树一样,但是在插入后:

  1. 将新插入的节点 X 标记为红色
  2. 如果 X 是根结点(root),则标记为黑色
  3. 如果 X 的 parent 不是黑色,同时 X 也不是 root:
  • 3.1 如果 X 的 uncle (叔叔) 是红色

    • 3.1.1 将 parent 和 uncle 标记为黑色

    • 3.1.2 将 grand parent (祖父) 标记为红色

    • 3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3

  • 3.2 如果 X 的 uncle (叔叔) 是黑色,我们要分四种情况处理

    • 3.2.1 左左 (P 是 G 的左孩子,并且 X 是 P 的左孩子)

    • 3.2.2 左右 (P 是 G 的左孩子,并且 X 是 P 的右孩子)

    • 3.2.3 右右 (P 是 G 的右孩子,并且 X 是 P 的右孩子)

    • 3.2.4 右左 (P 是 G 的右孩子,并且 X 是 P 的左孩子)

      (其实这种情况下处理就和我们的平衡二叉树一样了)

插入的动画演示:

它相比平衡二叉树,通过不严格平衡和改变颜色,就能在一定程度上减少旋转次数,这样的话对于整体性能是有一定提升的,只不过我们在插入结点时,就有点麻烦了,我们需要同时考虑变色和旋转这两个操作了,但是会比平衡二叉树更简单。

那么什么时候需要变色,什么时候需要旋转呢?我们通过一个简单例子来看看:

image

首先这棵红黑树只有一个根结点,因为根结点必须是黑色,所以说直接变成黑色。现在我们要插入一个新的结点了,所有新插入的结点,默认情况下都是红色:

image

所以新来的结点7根据规则就直接放到11的左边就行了,然后注意7的左右两边都是NULL,那么默认都是黑色,这里就不画出来了。同样的,我们往右边也来一个:

image

现在我们继续插入一个结点:

image

插入结点4之后,此时违反了红黑树的规则3,因为红色结点的父结点和子结点不能为红色,此时为了保持以红黑树的性质,我们就需要进行颜色变换才可以,那么怎么进行颜色变换呢?我们只需要直接将父结点和其兄弟结点同时修改为黑色(为啥兄弟结点也需要变成黑色?因为要满足性质5)然后将爷爷结点改成红色即可:

image

当然这里还需注意一下,因为爷爷结点正常情况会变成红色,相当于新来了个红色的,这时还得继续往上看有没有破坏红黑树的规则才可以,直到没有为止,比如这里就破坏了性质一,爷爷结点现在是根结点(不是根结点就不需要管了),必须是黑色,所以说还要给它改成黑色才算结束:

image

接着我们继续插入结点:

image

此时又来了一个插在4左边的结点,同样是连续红色,我们需要进行变色才可以讲解问题,但是我们发现,如果变色的话,那么从11开始到所有NIL结点经历的黑色结点数量就不对了:

image

所以说对于这种父结点为红色,父结点的兄弟结点为黑色(NIL视为黑色)的情况,变色无法解决问题了,那么我们只能考虑旋转了,旋转规则和我们之前讲解的平衡二叉树是一样的,这实际上是一种LL型失衡:

image

同样的,如果遇到了LR型失衡,跟前面一样,先左旋在右旋,然后进行变色即可:

image

而RR型和RL型同理,这里就不进行演示了,可以看到,红黑树实际上也是通过颜色规则在进行旋转调整的,当然旋转和变色的操作顺序可以交换。所以,在插入时比较关键的判断点如下:

  • 如果整棵树为NULL,直接作为根结点,变成黑色。
  • 如果父结点是黑色,直接插入就完事。
  • 如果父结点为红色,且父结点的兄弟结点也是红色,直接变色即可(但是注意得继续往上看有没有破坏之前的结构)
  • 如果父结点为红色,但父结点的兄弟结点为黑色,需要先根据情况(LL、RR、LR、RL)进行旋转,然后再变色。

在了解这些步骤之后,我们其实已经可以尝试去编写一棵红黑树出来了,当然代码太过复杂,这里就不演示了。其实红黑树难点并不在于如何构建和使用,而是在于,到底是怎么设计出来的,究竟要多么丰富的知识储备才能想到如此精妙的规则。

红黑树的发明者:

红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组

红黑树是在1972年由[Rudolf Bayer](https://baike.baidu.com/item/Rudolf Bayer/3014716)发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。


集合类

集合类的顶层都是接口,下面的每个类都是实现了上面的接口

image

不可变集合

这个功能在jdk9后才有!!!

不可变集合特点:定义完成后不可以修改、添加、删除 (和final关键字类似)

使用:调用静态方法of方法

List

List<String> list = List.of("柯南","毛利兰","灰原哀","阿笠博士");

Set

注意:这里定义的集合中不可有重复值,否则报错

Set<String> set = Set.of("柯南","毛利兰","灰原哀","阿笠博士");

Map

注意:键是不可重复的(下面的键是侦探n号)

当不可变参数的键值对在十对以内:

Map<String,String> map = Map.of("侦探一号","工藤新一","侦探二号","毛利小五郎","侦探三号","铃木园子","侦探四号","服部平次","侦探五号","白马探");

超过十对键值对,但是jdk9时:使用Map.ofEntries(xx); xx是一个数组,这个方法参数需要接收一个键值对数组

Map<String,String> map = new HashMap<>();
map.put("侦探一号","工藤新一");
map.put("侦探二号","毛利小五郎");
map.put("侦探三号","铃木园子");
map.put("侦探四号","服部平次");
map.put("侦探五号","白马探");

//toArray(xx)参数是指定返回的数组类型,所以传一个数组进去该数组类型就是这个方法返回的类型
Map.ofEntries(map.entrySet().toArray(new Map.Entry[0]));

超过十对键值对,且是jdk10时:

Map<String,String> map = new HashMap<>();
map.put("侦探一号","工藤新一");
map.put("侦探二号","毛利小五郎");
map.put("侦探三号","铃木园子");
map.put("侦探四号","服部平次");
map.put("侦探五号","白马探");

Map<String,String> newMap = Map.copyOf(map);

迭代器

应用例子可以看看前面foreach内容

Iterable和Iterator接口

每个集合类都有自己的迭代器,通过iterator()方法来获取:

Iterator<Integer> iterator = list.iterator();   //生成一个新的迭代器
while (iterator.hasNext()){    //判断是否还有下一个元素
  Integer i = iterator.next();     //获取下一个元素(获取一个少一个)
  System.out.println(i);
}

迭代器生成后,默认指向第一个元素,每次调用next()方法,都会将指针后移,当指针移动到最后一个元素之后,调用hasNext()将会返回false,迭代器是一次性的,用完即止,如果需要再次使用,需要调用iterator()方法。

ListIterator<Integer> iterator = list.listIterator();   //List还有一个更好地迭代器实现ListIterator

ListIterator是List中独有的迭代器,在原有迭代器基础上新增了一些额外的操作。

Set和Map撇不清的关系

特点:

HashSet:元素不重复、无序

HashMap:key不重复、key无序

LinkedHashSet:在HashSet基础上,会自动保存我们(访问)插入元素的顺序(set无法访问单独一个元素,因为元素无序)

LinkedHashMap:在HashMap基础上,会自动保存我们(访问)插入元素的顺序,对刚(访问)获取过的元素会将其位置放到最后

TreeSet:元素不重复,元素从大到小排列(默认)

TreeMap:key不重复,元素顺序按照key值从大到小排列(默认)

解释:

Hash 是用Hash表来存放数据

Tree 是用红黑树来存放数据(可以回顾红黑树特点)


Stream流

介绍:Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

stream流思想:像sql语句一样,对数据一步步的操作得到最终需要的结果。但执行的时候并不是像sql一样一句句的顺序执行,因为stream有指定的执行策略,流会将每次链式操作都记录下来,然后按照内置的链式优先级执行链式操作。如下图一样,会先执行中间方法 filter --> map --> skip (limit) 等再执行终结方法 count、collect、foreach【一个流中 中间方法可以有多个,但终结方法只能有一个】

image

image
stream流对集合类、工具类的基本操作:

流会将每次链式操作都记录下来,然后按照内置的链式优先级执行链式操作

//流对集合类操作
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);

list = list
        .stream()
        .distinct()   
        .sorted((a, b) -> b - a)
        .map(e -> {
            System.out.println(">>> "+e);   
            return e+1;
        })
        .limit(2)   
        .collect(Collectors.toList());
//实际上,stream会先记录每一步操作,而不是直接开始执行内容,当整个链式调用完成后,才会依次进行!


//流对工具类操作
public static void main(String[] args) {
    Random random = new Random();  //Random是一个随机数工具类
    random
            .ints(-100, 100)   //生成-100~100之间的,随机int型数字(本质上是一个IntStream)
            .limit(10)   //只获取前10个数字(这是一个无限制的流,如果不加以限制,将会无限进行下去!)
            .filter(i -> i < 0)   //只保留小于0的数字
            .sorted()    //默认从小到大排序
            .forEach(System.out::println);   //依次打印
}

将工具类生成一个统计实例实现快速统计:

IntSummaryStatistics 该类只能对静态数据操作(数组、random随机数),不可以是集合类

public static void main(String[] args) {   
    //工具类生成统计实例
    Random random = new Random();  //Random是一个随机数工具类
    IntSummaryStatistics statistics = random
            .ints(0, 100)
            .limit(100)	//这里不限制则会无线的生成随机数
            .summaryStatistics();    //获取语法统计实例
    System.out.println(statistics.getMax());  //快速获取最大值
    System.out.println(statistics.getCount());  //获取数量
    System.out.println(statistics.getAverage());   //获取平均值
}

获取流的方法

有三种情况可以获取流:(下面双列集合(map)不能直接获取流,但可以转成单列集合来获取)

获取方式方法名说明
单列集合default Stream stream()Collection中的默认方法
双列集合无法直接使用stream流
数组public static Stream stream(T[] array)Arrays工具类中的静态方法
一堆零散数据public static Stream of(T… values)Stream接口中的静态方法

单列集合

使用list.stream()

list.stream().forEach(s -> System.out.println(s));

双列集合

以map为例

  1. 方式一:健值分别获取流 map.keySet().stream() map.values().stream()

    HashMap<String, Integer> map = new HashMap<>();
    //获取键的流
    map.keySet().stream().forEach(s -> System.out.println(s));
    //获取值的流
    map.values().stream().forEach(s -> System.out.println(s));
  2. 方式二:键值对作为整体来获取流

    HashMap<String, Integer> map = new HashMap<>();
    map.entrySet().stream().forEach(s -> System.out.println(s.getKey() + ":" + s.getValue()));

数组

使用Arrays.stram(arr)

int[] arr = {1, 2, 3};
Arrays.stream(arr).forEach(s -> System.out.println(s));

零散数据

注意这里虽然是叫零散数据,但必须保证零散的数据是同一类型的

使用Stream.of(T... values) 形参是一个可变参数,可变参数的底层是数组(所以可以传一个数组、列表、集合等)

Stream.of(1,2,3,8,6).forEach(s -> System.out.println(s));

注意:

传来的数组必须是包装类数组。因为将基本数据类型的数组到参数中,该数组会被当成一个数据而不是一组数组。

//基本数据类型
int[] arr = {1, 2, 3};
//引用数据类型
String[] arr2 = {"a", "b", "c"};
// 基本数据类型
Stream.of(arr).forEach(s -> System.out.println(s)); // [I@7c3df479 打印的是地址
// 引用数据类型
Stream.of(arr2).forEach(s -> System.out.println(s));// a b c

Stream的中间方法

filter 过滤

符合条件的数据才留下,不符合的就去除

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

/*匿名内部类写法:*/
// filter 过滤:把姓毛利的留下
list.stream().filter(new Predicate<String>() { //这里的String只是数据的类型
    @Override
    public boolean test(String s) {	//这里的String只是数据的类型
        // 如果返回值为true,表示当前数据保留,否则舍弃
        return s.startsWith("毛利");
    }
}).forEach(s -> System.out.println(s));	//毛利兰,毛利小五郎


/* lamdba写法:*/
list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));//毛利兰,毛利小五郎

map 转换

转换流中的数据类型,也可以修改数据,不改变原来集合list的数据

List<String> list = new ArrayList<>();
Collections.addAll(list, "工藤新一-17", "灰原哀-12", "毛利兰-16",
                   "白马探-20", "阿笠博士-55", "毛利小五郎-40", "目暮警官-35", "佐藤-28", "高木-29");
// 目的:只获取其中的年龄 String -> int

/*匿名内部类写法:*/
// 第一个参数类型:流中原本的数据类型
// 第二个参数类型:要转成之后的类型
list.stream().map(new Function<String, Integer>() {
    // apply的形参s:依次表示流中的每一个数据
	// 返回值:表示转换之后的数据
    @Override
    public Integer apply(String s) {
        String[] split = s.split("-");
        Integer age = Integer.valueOf(split[1]);
        return age;
    }
}).forEach(s -> System.out.print(s + " "));		//17 12 16 20 55 40 35 28 29

/* lamdba写法:*/
list.stream()
    .map(s -> Integer.valueOf(s.split("-")[1]))	//s代表每个数据,箭头后面是返回值
    .forEach(s -> System.out.print(s + " "));   // 17 12 16 20 55 40 35 28 29

limit 保留 skip 跳过

limit:只保留前n个数据 skip:跳过前n个数据

注:limit和skip是同优先级的,所以先写哪个就执行哪个

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

//目的:只得到"基德", "服部平次", "阿笠博士"
list.stream().skip(3).limit(3).forEach(s -> System.out.print(s + " ")); //"基德", "服部平次", "阿笠博士"

list.stream().limit(6).skip(3).forEach(s -> System.out.print(s + " "));	//"基德", "服部平次", "阿笠博士"

distinct 去重

distinct : 元素去重,依赖(hashCode方法和equals方法)

底层利用的是 hashSet 去重的
注:hashSet 存储自定义对象的时候要重写hashCode和equals方法

List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "毛利兰","毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");



list1.stream().distinct().forEach(s -> System.out.print(s + " "));
// "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"

concat 合并

使用Stream.concat(list1.stream(), list2.stream()) 合并两个流

List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "毛利兰","毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
List<String> list2 = new ArrayList<>();
Collections.addAll(list2, "赤井秀一", "雪莉");

// concat :合并a和b为一个流,如果两个流的类型不一致,会合并到它们的父类
Stream.concat(list1.stream(), list2.stream())
    .forEach(s -> System.out.print(s + " "));
// "毛利兰","毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀","赤井秀一", "雪莉"

Stream的终结方法

一般都是流的最后调用的方法

forEach

遍历每个参数

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

/*匿名内部类写法:*/
// Consumer的泛型:表示流中数据的类型
// accept() 方法中的形参s:依次表示流中的每一个数据
// 方法体:对每一个数据进行处理
list.stream().forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        // 每次消耗一个数据
        System.out.print(s + " ");
    }
});  //"毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"


/*lambda写法:*/
list.stream().forEach(s -> System.out.print(s + " "));
// "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"

count

计算元素个数

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

long count = list.stream().count(); //count=7

toArray

收集流中的数据,放到数组中

  • 无参的写法:返回的是Object数组【不推荐】

    List<String> list = new ArrayList<>();
    Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
    
    Object[] arr1 = list.stream().toArray();
    // [毛利兰, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]
  • 有参的写法:可以指定返回数组的类型【推荐】

    List<String> list = new ArrayList<>();
    Collections.addAll(list, "毛利兰", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
    
    /*匿名内部类写法:*/
    // IntFunction的泛型:具体类型的数组
    // apply的形参value:流中数据的个数,要和数组的长度保持一致
    // apply的返回值:具体类型的数组
    
    // toArray 方法参数的作用:负责创建一个指定类型的数组
    // toArray 方法的底层:会一次得到流里面的每一个数据,并把数据放到数组中去
    // tiArray 方法的返回值:是一个装着流里面所有数据的数组
    String[] arr2 = list.stream().toArray(new IntFunction<String[]>() {
        @Override
        public String[] apply(int value) {
            return new String[value];
        }
    });
    System.out.println(Arrays.toString(arr2));
    //[毛利兰, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]
    
    /* lambda写法:*/
    String[] arr3 = list.stream().toArray(value -> new String[value]);
    // [毛利兰, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]

collect

collect(Collector collector) : 收集流中的数据,放到集合中(List,Set,Map)

List
List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13",
                   "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有男性放到一个List集合中
List<String> collect1 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toList());
// [柯南-男-15, 服部平次-男-20, 阿笠博士-男-100, 毛利小五郎-男-40, 目暮警官-男-35, 高木-男-37, 琴酒-男-50]
Set

若数据中有重复的值,则最后toSet()的时候会删除重复值。也是set集合的特点

List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13","服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有男性放到一个Set集合中
Set<String> collect2 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toSet());
System.out.println(collect2);
// [目暮警官-男-35, 琴酒-男-50, 服部平次-男-20, 阿笠博士-男-100, 柯南-男-15, 毛利小五郎-男-40, 高木-男-37]
map
List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13",
                   "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有的男性收集到Map集合中   键:姓名   值:年龄	

/**
*  toMap :
*      参数1:表示键的生成规则
*      参数2:表示值的生成规则
*  参数1:
*      Function:
*          泛型1:表示流中每一个数据的类型
*          泛型2:表示Map集合中键的数据类型
*      方法apply形参:依次表示流里面的每一个数据
*          方法体:生成键的代码
*          返回值:已经生成的键
*  参数2:
*      Function:
*          泛型1:表示流中每一个数据的类型
*          泛型2:表示Map集合中值的数据类型
*      方法apply形参:依次表示流里面的每一个数据
*          方法体:生成值的代码
*          返回值:已经生成的值
*
*  注意:这里Map的键不能重复,否则会报错
*/

/*匿名内部类写法:*/
Map<String, Integer> map = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(new Function<String, String>() {
        @Override
        public String apply(String s) {
            return s.split("-")[0];
        }
    }, new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
            return Integer.valueOf(s.split("-")[2]);
        }
    }));
System.out.println(map);
// {目暮警官=35, 服部平次=20, 毛利小五郎=40, 高木=37, 阿笠博士=100, 柯南=15, 琴酒=50}


/* lambda写法*/
Map<String, Integer> map2 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(
        s -> s.split("-")[0],
        s -> Integer.valueOf(s.split("-")[2])));
System.out.println(map2);
// {目暮警官=35, 服部平次=20, 毛利小五郎=40, 高木=37, 阿笠博士=100, 柯南=15, 琴酒=50}
}

练习

  1. 将字符串数据转换成对象List

    //要求:将男生,女生名字都为三个字的封装到detective类中
    
    List<String> manList = new ArrayList<>();
    List<String> womanList = new ArrayList<>();
    
    Collections.addAll(manList, "柯南-男-15", "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "琴酒-男-50");
    Collections.addAll(womanList,  "灰原哀-女-14", "步美-女-13",  "雪莉-女-50");
    
    Stream<String> manStream = manList.stream().filter(s -> s.split("-")[0].length() > 2);
    Stream<String> womanStream = womanList.stream().filter(s -> s.split("-")[0].length() > 2);
    
    List<Detective> collect = Stream.concat(manStream, womanStream)
        .map(s -> new Detective(s.split("-")[0], Integer.valueOf(s.split("-")[2])))
        .collect(Collectors.toList());
    //[Detective(name=服部平次, age=20), Detective(name=阿笠博士, age=100), Detective(name=毛利小五郎, age=40), Detective(name=灰原哀, age=14)]

常用API

静态的方法,都是直接类名+方法名 的方式调用的

Math

public static int abs(int a);                   //获取参数绝对值
public static double ceil(double a);            //向上取整
public static double floor(double a);           //向下取整
public static int round(float a);               //四舍五入
public static int max(int a,int b);             //获取两个int值中的较大值
public static double pow(double a,double b);    //返回a的b次幂(一般是处理大于1的幂)
public static double sqrt(double a);            //返回a的平方根
public static double cbrt(double a);            //返回a的立方根
public static double random();                  //返回值为double的随机值,范围[0.0,1.0)

注:abs有bug。int类型的a取值范围只能是 -2147483648 ~ 2147483647 超过就取不到绝对值。此时可以使用Math.absExact(int a); 这个方法遇到不在范围内的数会报错,但这个方法只在jdk15后才有

System

public static void exit(int status);                                        //终止当前运行的Java虚拟机
public static long currentTimeMillis();                                     //返回当前系统的时间(毫秒为单位)
public static void arraycopy(数据源数组,初始索引,目的地数组,起始索引,拷贝个数);   //数组拷贝
  1. exit( int status ); 状态码0表示虚拟机正常停止,非0表示虚拟机非正常停止

    底层调用的是Runtime的exit方法——停止虚拟机

  2. 数组拷贝注意点:

    • 数据源和目的地数组都是基本数据类型,那么两者的类型必须保持一致,否则报错
    • 拷贝超过数组长度也会报错
    • 若数据源和目的地数组都是引用数据类型(对象类型等),那么子类类型可以赋值给父类类型

RunTime

该类的方法不是static,需要创建对象才能使用。但这里的对象不能手动创建而是需要调用getRuntime方法创建,这是饿汉式的设计模式,也保证了该类对象只有一个,不能有多个。(保证虚拟机在当前电脑中的唯一性)

public static Runtime getRuntime(); //当前系统的运行环境对象
public void exit(int status);       //停止虚拟机【状态码0是正常停止,非0是不正常停止】
public int availableProcessors();   //获得CPU的线程数
public long maxMemory();            //JVM能从系统中获取总内存大小(单位byte)
public long totalMemory();          //JVM已经从系统中获取总内存大小(单位byte)
public long freeMemory();           //JVM剩余内存大小(单位byte)
public Process exec(String command);//运行cmd命令【这里有bug,执行不了dir。因为本身在的目录条件受限】
  1. 调用方法:Runtime.getRuntime().exit(0);
  2. Runtime.getRuntime().exec("shutdown -s -t 1200"); 是在1200秒后自动关机
    • shutdown -s:默认在1分钟后关机
    • shutdown -s -t xx:在xx秒后关机
    • shutdown -a:取消关机操作
    • shutdown -r:关机并重启

Object

public String toString();           //返回对象的Hash地址
public boolean equals(Object obj);  //比较两对象地址值是否相等
protected Object clone(int a);      //对象克隆【浅克隆】
  1. 所有类都默认继承Object,Object的toString方法默认是返回对象的Hash地址。我们平常使用toString方法打印对象or字符串是因为toString方法被重写了,子类可以重写父类的方法。

  2. 我们平常使用的equals是不一样的。每种类里的equals的底层逻辑可能不同【某大厂面试题】

    • String中的equals方法

      String str="abc";
      StringBuilder sb =new StringBuilder("abc");
      
      System.out.println(str.equals(sb)); //false
      
      原因:这里调用的是String的equals,其源码是先判断是不是相同地址,是的话直接返回true。不相同地址再看比较对象是不是字符串,若不是直接false,是字符串才会继续比较两字符串的值是否相同。
    • StringBuilder中的equals方法

      String str="abc";
      StringBuilder sb =new StringBuilder("abc");
      
      System.out.println(sb.equals(str)); //false
      原因:StringBuilder没有重写equals方法,所以是用Object的equals方法,只比较对象地址值是否相同
  3. clone的使用

    看源码可知Object中的clone方法是protected的

    • 重写Object中的clone方法(这里其实在实体类中继承父类的clone即可 super.clone() )
    • 让实体类实现Cloneable接口
    • 在业务层创建原对象并调用即可

Objects

public static boolean equals(Object a,Object b);    //先做非空判断,在调用重写的equals方法
public static boolean isNull(Object obj);           //判断对象是否为null
public static boolean nonNull(Object obj);          //判断对象是否为非null
  1. equlas这个工具类方法就避免了比较时有null报错的问题。先判断非空,如何在调用equals方法,如a中重写了equals方法则优先会使用重写后的方法

BigInteger

方法都是非静态的,所以调用方法需要创建对象

优点:可以对很大的整数进行运算

能保存十分大的整数的原理:将很大的数字进行了拆分装进数组,第一组只有一个数代表符合位,后面每32位分成一组来计算

构造方法:

public BigInteger(int num, Random rnd) 		//获取随机大整数,范围:[0~ 2的num次方-11
public BigInteger(String val) 				//获取指定的大整数	【val必须是整数】
public BigInteger(String val, int radix)	//获取指定进制的大整	【radix必须和val进制相同,否则会报错】
public static BigInteger valueOf(long val)	//静态方法获取BigInteger的对象,内部有优化
  1. 创建对象的两种方式

    • 数较小的时候(long类型内)

      源码中将 -16~16优化了,这个区间的数被提前创建了对象装到了数组中,所以多次对这区间内同一个数创建对象,实际是取数组内的同一个对象

      //静态方法创建对象
      BigInteger bigInteger = BigInteger.valueOf(123456);
    • 数较大的时候(大于long类型)

      //构造方法创建对象
      BigInteger b = new BigInteger("123");	//这里一个参数的时候只能是字符串
  2. 创建一个指定进制的大整数(将一个指定进制的字符串转成十进制的大整数)

    BigInteger b2 = new BigInteger("100101011", 2); //这里b2=299 是一个整数

一般方法:

public BigInteger add(BigInteger val) 					//加法
public BigInteger subtract(BigInteger val) 				//减法
public BigInteger multiply(BigInteger val) 				//乘法
public BigInteger divide(BigInteger val) 				//除法,获取商
public BigInteger[] divideAndRemainder(BigInteger val) 	//除法,获取商和余数【数组 0位为商,1位为余数】
public boolean equals(Object x) 						//比较是否相同【由于重写了equls方法,所以这里是比较值】
public BigInteger pow(int exponent) 					//次幂 
public BigInteger max/min(BigInteger val)	 			//返回较大/小值对象【这里返回的是较大/小的那个对象的引用】
public int intValue(BigInteger val) 					//转为int类型整数,超出范围数据有误
public long longValue(BigInteger val) 					//转为long类型整数,超出范围数据有误
public double doubleValue(BigInteger val) 					//转为double类型整数,超出范围数据有误
  1. BigInteger对象一旦创建,内部的数据不能发生改变

    每次的运算都会返回一个新的对象

    BigInteger bd9 =BigInteger.valueOf(1);
    BigInteger bd10 =BigInteger.valueOf(2);
    //此时,不会修改参与计算的BigInteger对象中的借,而是产生了一个新的BigInteger对象记录
    BigInteger result=bd9.add(bd10); //result=3
  2. 除法得到商和余数

    BigInteger bd1 = BigInteger.valueOf(10);
    BigInteger bd2 = BigInteger.valueOf(3);
    BigInteger[] arr = bd1.divideAndRemainder(bd2);
    System.out.println(arr[0]);	//3
    System.out.println(arr[1]);	//1

BigDecimal

方法都是非静态的,所以调用方法需要创建对象

优点:可以保存十分大的浮点数可以精准处理浮点数之间的运算

能保存十分大的浮点数的原理:将浮点数都转成字符串后每个符号和数字都拆分出来,数组保存每个字符对应的ascii码

构造方法:

//构造方法获取BigDecimal对象
public BigDecimal(double val) public BigDecimal(string val)
//静态方法获取BigDecimal对象
public static BigDecimal valuef(double val)
  1. 创建对象的三种方法:

    • 当数较小的时(double内) 以及 想要精准计算浮点数的时:使用静态方法【推荐】

      如果我们传递的是0~10之间的整数,包含0,包含10,那么方法会返回已经创建好的对象,不会重新new

      BigDecimal decimal = BigDecimal.valueOf(4869.00);
    • 当数较大时:使用字符串型的构造方法【推荐】

      BigDecimal decimal1 = new BigDecimal("123.00");
    • 当数较大时:使用浮点数型的构造方法【不推荐】

      因为用浮点数创建对象会导致创建的数有误差。而前面静态方法不会是因为做了处理转成了字符串

      BigDecimal decimal2 = new BigDecimal(123.00);

一般方法:

public BigDecimal add(BigDecimal val)						//加法
public BigDecimal subtract(BigDecimal val) 					//减法
public BigDecimal multiply(BigDecimal val) 					//乘法
public BigDecimal divide(BigDecimal val) 					//除法
public BigDecimal divide(BigDecimal val,精确几位,舍入模式)	 //除法
  1. BigDecimal对象一旦创建,内部数据不能改变

    BigDecimal bd1 = BigDecimal.valueOf(10.0);
    BigDecimal bd2 = BigDecimal.valueOf(2.0);
    BigDecimal bd3 = bd1.add(bd2); //bd3=12.00
  2. 除法,一般都需要指定精确几位和舍入模式

    //RoundingMode.HALF_UP是指四舍五入 (其他的舍入模式需要去看这个类RoundingMode里的枚举)
    BigDecimal bd6 = bd1.divide(bd2, 2, RoundingMode.HALF_UP); 	//bd6=3.3

Optional类

Optional类是Java8为了解决null值判断问题,使用Optional类可以避免显式的null值判断(null的防御性检查),避免null导致的NPE(NullPointerException)。总而言之,就是对控制的一个判断,为了避免空指针异常。

工作常用方式:

List<String> list = null;	//外界得到的集合
List<String> newList = Optional.ofNullable(list).orElse(xxxx);	//如果list非空则返回参数list,如果list是空则返回后面参数xxxx

Arrays工具类

Arrays是对数组操作的工具类。

基本操作:

Arrays.toString(arr);	//将数组转为字符串输出

Arrays.sort(arr);	//将数组正序排列(从小到大),改变本身不返回数组
Arrays.sort(arr,Collections.reverseOrder()); 	//将数组逆序排列,改变本身不返回数组
    
Arrays.equals(arr01,arr02);		//比较两数组值是否相同

Arrays.binarySearch(arr,key);	//用二分搜索寻找arr数组中是否有key值,有则返回下标

arr02=Arrays.copyOf(arr01,arr01.length);	//从0到length-1复制arr01数组,返回新的数组

一般数组转集合类:1. 先转为固定长度的ListArrays.asList(array) 2. 再转为集合类

Integer[] array = {1, 5, 2, 4, 7, 3, 6};
List<Integer> list = new ArrayList<>(Arrays.asList(array));

Collections工具类

Collections是对集合类操作的工具类

基本操作:

排序操作
1.		reverse(List):反转List中元素的顺序
2.		shuffle(List):对List集合元素进行随机排序
3.		sort(List):根据元素的自然顺序对指定List集合元素按升序排序
4.		sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序

替换 和 查找操作
5.		swap(List, int ,int ):将指定List集合中的 i 处元素 和 j 处元素进行交换
6.		Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
7.      Object max(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
8.		Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
9.		Object min(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
10.		int frequency(Collection,Object):返回指定集合中指定元素的出现次数
11.		void copy(List dest,List src):将src中的内容复制到dest中
        注意复制的目标集合的长度必须大于源集合,否则会抛出空指针异常
    
12.     boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值

使用方式:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    max = Collections.max(list);
    miin = Collections.min(list);
}

时间API

以下都是JDK1.8新增的时间方法 优点:使用更加方便、解决了多线程带来的线程安全问题

ZoneId 时区(重点
static Set<string> getAvailableZoneIds() 	//获取Java中支持的所有时区
static ZoneId systemDefault() 				//获取系统默认时区
static Zoneld of(string zoneld) 			//获取一个指定时区
  1. 获取所有的时区名称

    Set<String> zoneIds = ZoneId.getAvailableZoneIds(); //zoneIds集合中有六十个时区名,格式:州/城市
    System.out.println(zoneIds.size());//600
    System.out.println(zoneIds);// Asia/Shanghai
  2. 获取当前系统的默认时区

    ZoneId zoneId = ZoneId.systemDefault(); //zoneId= Asia/Shanghai
  3. 获取指定的时区【不知道指定时区的写法可以通过前面方法获取所有,然后找需要的时区】

    ZoneId zoneId1 = ZoneId.of("Asia/Pontianak");//zoneId= Asia/Pontianak

Instant 时间戳

static Instant now() 					//获取当前时间的Instant对象(标准时间)
static Instant ofXxxx(long epochMilli) 	//根据(秒/毫秒/纳秒)获取Instant对象	
ZonedDateTime atZone(ZoneIdzone) 		//指定时区
boolean isxxx(Instant otherInstant) 	//判断系列的方法
Instant minusXxx(long millisToSubtract) //减少时间系列的方法
Instant plusXxx(long millisToSubtract) 	//增加时间系列的方法
  1. 根据(秒/毫秒/纳秒)获取Instant对象【了解】

    Instant instant1 = Instant.ofEpochMilli(0L);
    System.out.println(instant1);//1970-01-01T00:00:00z
    
    Instant instant2 = Instant.ofEpochSecond(1L);
    System.out.println(instant2);//1970-01-01T00:00:01Z
    
    Instant instant3 = Instant.ofEpochSecond(1L, 1000000000L);
    System.out.println(instant3);//1970-01-01T00:00:027
  2. 获取指定时区的时间戳【了解】

    ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai")); //time就是现在上海时区的时间
  3. 判断时间前后【了解,主要用后面的LocalDateTime的方法】

    Instant instant4=Instant.ofEpochMilli(0L);
    Instant instant5 =Instant.ofEpochMilli(1000L);
    
    //isBefore:判断调用者代表的时间是否在参数表示时间的前面
    boolean result1=instant4.isBefore(instant5);
    System.out.println(result1);//true
    
    //isAfter:判断调用者代表的时间是否在参数表示时间的后面
    boolean result2 = instant4.isAfter(instant5);
    System.out.println(result2);//false

ZonedDateTime

static ZonedDateTime now() 					//获取当前时间的ZonedDateTime对象
static ZonedDateTime ofXxxx(。。。) 			//获取指定时间的ZonedDateTime对象
ZonedDateTime withXxx						//(时间) 修改时间系列的方法
ZonedDateTime minusXxx(时间)			 		//减少时间系列的方法
ZonedDateTime plusXxx(时间) 					//增加时间系列的方法

DateTimeFormatter 格式化(重点

static DateTimeFormatter ofPattern		//(格式) 获取格式对象
String format							//(时间对象) 按照指定方式格式化
  1. 设置时间格式【重点】

    //y:年份 M:月份 d:几号   H:小时  m:分钟  s:秒   E:星期几   a:上午or下午 【-和: 符号可以自由更改】
    DateTimeFormatter dtf1=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");
  2. 格式化时间

    //获取时间对象
    ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
    // 解析/格式化器
    DateTimeFormatter dtf1=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm;ss EE a");
    // 格式化
    System.out.println(dtf1.format(time));

LocalDate

只能获取年月日,并且只能对年月日操作 (由于使用较少就不描述了)

LocalTime

只能获取时分秒毫秒,并且只能对时分秒毫秒操作 (由于使用较少就不描述了)

LocalDateTime(重点

构造方法

//1.创建当前时间对象
LocalDateTime localDateTime = LocalDateTime.now(); //这里可以放参数指定时区
//2.创建指定时间的对象
LocalDateTime localDateTime = LocalDateTime.of(2022, 10, 06, 18, 00);//指定时间可以具体到纳秒

一般方法

LocalDateTime localDateTime = LocalDateTime.now();

localDateTime//今天的日期
localDateTime.getYear()//年
localDateTime.getMonthValue()//月
localDateTime.getDayOfMonth()//日
localDateTime.getHour()//时
localDateTime.getMinute()//分
localDateTime.getSecond()//秒
localDateTime.getNano()//纳秒

localDateTime.getDayOfYear()				//当年的第几天

localDateTime.getDayOfWeek()				//返回英文写法的星期几
localDateTime.getDayOfWeek().getValue()		//返回数字星期几
    
localDateTime.getMonth()					//返回英文写法的月份
localDateTime.getMonth().getValue()			//返回数字的第几月
localDateTime.getDayOfMonth()				//返回该时间是当月第几天
    
LocalDate ld = localDateTime.toLocalDate();	//转成LocalDate
LocalTime lt = localDateTime.toLocalTime();	//转成LocalTime

//指定日期格式并设置到当前时间
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");
localDateTime.format(dateTimeFormatter);	

//判断时间前后
localDateTime.isAfter(time01); 				//返回localDateTime是否在time01后面
localDateTime.isBefore(time01); 			//返回localDateTime是否在time01前面

//在时间对象基础上修改某个年份/月份/..  withxxx就是修改xxx的数据 【返回一个新的对象(因为时间是不可变对象)】
LocalDateTime time1 = localDateTime.withYear(2000);

//在时间对象基础上增加一段时间  plusxxx就是增加xxx的数据 【返回一个新的对象(因为时间是不可变对象)】
LocalDateTime time02 = localDateTime.plusDays(100);

//在时间对象基础上增加一段时间  minxxx就是较少xxx的数据 【返回一个新的对象(因为时间是不可变对象)】
LocalDateTime time03 = localDateTime.minusDays(99);

Period

主要作用是时间作差,这个只能对年月日作差 (由于使用较少就不描述了)

Duration

主要作用对时间作差,这个只能对时分秒毫秒作差(由于使用较少就不描述了)

ChronoUnit 时间作差(重点

// 当前时间
LocalDateTime today = LocalDateTime.now();
System.out.println(today);
// 生日时间
LocalDateTime birthDate = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
System.out.println(birthDate);

System.out.println("相差的年数:" + ChronoUnit.YEARS.between(birthDate, today));
System.out.println("相差的月数:" + ChronoUnit.MONTHS.between(birthDate, today));
System.out.println("相差的周数:" + ChronoUnit.WEEKS.between(birthDate, today));
System.out.println("相差的天数:" + ChronoUnit.DAYS.between(birthDate, today));
System.out.println("相差的时数:" + ChronoUnit.HOURS.between(birthDate, today));
System.out.println("相差的分数:" + ChronoUnit.MINUTES.between(birthDate, today));
System.out.println("相差的秒数:" + ChronoUnit.SECONDS.between(birthDate, today));
System.out.println("相差的毫秒数:" + ChronoUnit.MILLIS.between(birthDate, today));
System.out.println("相差的微秒数:" + ChronoUnit.MICROS.between(birthDate, today));
System.out.println("相差的纳秒数:" + ChronoUnit.NANOS.between(birthDate, today));
System.out.println("相差的半天数:" + ChronoUnit.HALF_DAYS.between(birthDate, today));
System.out.println("相差的十年数:" + ChronoUnit.DECADES.between(birthDate, today));
System.out.println("相差的世纪(百年)数:" + ChronoUnit.CENTURIES.between(birthDate, today));
System.out.println("相差的千年数:" + ChronoUnit.MILLENNIA.between(birthDate, today));
System.out.println("相差的纪元数:" + ChronoUnit.ERAS.between(birthDate, today));

包装类

这里补充一个知识点:自动装箱、自动拆箱(jdk5后的新功能)

没有自动装/拆箱时,包装类运算:

Integer i1 = new Integer(1);
Integer i2 = new Integer(2);
int result = i1.intValue() + i2.intValue();
Integer i3 = new Integer(result);
System.out.println(i3);

有自动装/拆箱时,包装类运算:

Integer i1 = new Integer(1);
Integer i2 = new Integer(2);
Integer i3 = i1+i2;
System.out.println(i3);

自动装箱:基本数据类型可以直接被赋值到包装类上

Integer i = 10;	//基本数据类型被自动装箱成了包装类

自动拆箱:

Integer i2 = new Integer(10);
//自动拆箱的动作。包装类被自动拆箱成了基本数据类型
int i = i2;

java有八大基本包装类: Byte、Character、Double、Integer、Float、Long、Short、Boolean

因为每个包装类都相似,所以这里以Integer为例

Integer

构造方法

//new的构造方法已经过时,在jdk5之后有了自动拆箱和装箱
public Integer(int value) 							//根据传递的整数创建一个Integer对象  
public Integer(String s) 							//根据传递的字符串创建一个Integer对象
    
//valueOf底层是对-127~128进行了优化,这个区间的数被提前创建了对象装到了数组中,所以多次对这区间内同一个数创建对象,实际是取数组内的同一个对象【原因:这个范围内的数字使用率高,所以不重复创建对象浪费内存】
public static Integer valueOf(int i) 				//根据传递的整数创建一个Integer对象
public static Integer valueof(String s) 			//根据传递的字符串创建一个Integer对象
public static Integer valueof(String s, int radix) 	//根据传递的字符串和进制创建一个Integer对象

成员方法

public static string tobinarystring(int i) 			//得到二进制
public static string tooctalstring(int i) 			//得到八进制
public static string toHexstring(int i) 			//得到十六进制
public static int parseInt(string s) 				//将字符串类型的整数转成int类型的整数
  1. 将一个int类型数字转成指定的进制,返回字符串

    String str1 = Integer.toBinaryString(100);	//转成二进制字符串
    String str2 = Integer.toOctalString(100);	//转成八进制字符串
    String str3 = Integer.toHexString(100);		//转成十六进制字符串
  2. 字符串数字装成int类型的整数【8种包装类当中,除了Character都有对应的parseXxx的方法,进行类型转换】

    细节:参数只能是数字字符串,否则报错

    int i = Integer.parseInt("123");

JDK1.8新增方法

集合类的compute

用于替换更新数据

public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "A");
    map.put(2, "B");
    
    //不建议
    map.compute(1, (k, v) -> {   //compute会将指定Key(也就是1)的值进行重新计算,若Key不存在,v会返回null
        return v+"M";     //这里返回原来的value+M
    });
    
    //建议使用
  	map.computeIfPresent(1, (k, v) -> {   //当Key(也就是1)存在时存在则计算并赋予新的值
      return v+"M";     //这里返回原来的value+M
    });
    System.out.println(map);
    
    //建议使用
    map.computeIfAbsent(0, (k) -> {   //若key(也就是0)不存在则计算并插入新的值
        return "M";     //这里返回M
    });
}

集合类的merge

用于处理数据

public static void main(String[] args) {
    List<Student> students = Arrays.asList(
            new Student("yoni", "English", 80),
            new Student("yoni", "Chiness", 98),
            new Student("yoni", "Math", 95),
            new Student("taohai.wang", "English", 50),
            new Student("taohai.wang", "Chiness", 72),
            new Student("taohai.wang", "Math", 41),
            new Student("Seely", "English", 88),
            new Student("Seely", "Chiness", 89),
            new Student("Seely", "Math", 92)
    );
    Map<String, Integer> scoreMap = new HashMap<>();
    //merge()是合并,用于key相同但value不同的数据处理,最后一个参数就是数据处理的具体方式(对value的操作)
    students.forEach(student -> scoreMap.merge(student.getName(), student.getScore(), (a,b)->a+b));
    scoreMap.forEach((k, v) -> System.out.println("key:" + k + "总分" + "value:" + v));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值