JAVA基础知识(一)

81 阅读1小时+

java里的类和js样一样吗?有constructor吗?⭐⭐

不太一样,js中有constructor,而java中不是constructor,是和类名相同的名字。

构造器是一种特殊类型的方法,用于初始化新创建的对象。构造器的名字与类名相同,并且没有返回类型(连void也没有)。每当一个新对象被创建时,构造器会被自动调用。

public class Person{ 
String name;
int age; 
// 构造器
public Person(String name, int age) {
this.name = name;
this.age = age; 
} 
// 其他方法...
}

在这个例子中,Person类有一个接受两个参数(nameage)的构造器。当你创建Person类的一个新实例时,如Person person = new Person("Alice", 30);,这个构造器会被调用来设置nameage的值。

为什么java中不能打印对象数组?⭐⭐

public class Student {
    int number;
    int grade;
    int score;

    public void info() {
        System.out.println("number:" + number + ",state:" + grade + ",score:" + score);
    }
}
public class StudentTest {
    public static void main(String[] args) {
            //首先创建数组
        Student[] students = new Student[20];
            //通过循环结构给数组的属性赋值
        for(int i = 0;i < students.length;i++) {
            //数组元素的赋值
            students[i]=new Student();
            //数组元素是一个对象,给对象的各个属性赋值
            students[i].number=(i+1);
            students[i].grade=(int)(Math.random()*6+1);//[1,6]
            students[i].score=(int)(Math.random()*101);//[0,100]
        }
        System.out.println(Arrays.toString(students));
    }
}
  1. 为什么打印出来的值是 [com.baidu.xiaolong.Student@4eec7777, com.baidu.xiaolong.Student@3b07d329, com.baidu.xiaolong.Student@41629346, com.baidu.xiaolong.Student@404b9385, com.baidu.xiaolong.Student@6d311334, com.baidu.xiaolong.Student@682a0b20, com.baidu.xiaolong.Student@3d075dc0, com.baidu.xiaolong.Student@214c265e, com.baidu.xiaolong.Student@448139f0, com.baidu.xiaolong.Student@7cca494b, com.baidu.xiaolong.Student@7ba4f24f, com.baidu.xiaolong.Student@3b9a45b3, com.baidu.xiaolong.Student@7699a589, com.baidu.xiaolong.Student@58372a00, com.baidu.xiaolong.Student@4dd8dc3, com.baidu.xiaolong.Student@6d03e736, com.baidu.xiaolong.Student@568db2f2, com.baidu.xiaolong.Student@378bf509, com.baidu.xiaolong.Student@5fd0d5ae, com.baidu.xiaolong.Student@2d98a335]
  • 因为JAVA中只能打印基本数据类型,如果打印引用数据类型,打印出来的是引用类型的地址,我们可以使用for循环对内容进行打印

System.out.println(employees[i]);请问这里调用这句代码,会调用他的toString方法是吗?

是的,当你使用 System.out.println(employees[i]); 这条语句时,如果 employees[i] 不是一个基本数据类型(如 int、double 等),而是像 Employee 这样的对象类型,那么 Java 会自动调用该对象的 toString() 方法来获取其字符串表示形式,并将该字符串输出到控制台。

在 Java 中,几乎所有的类都继承自 Object 类,而 Object 类提供了一个 toString() 方法。这个方法返回一个字符串,其内容是对象的哈希码的无符号十六进制表示。然而,由于这个默认的 toString() 方法通常不提供太多有用的信息,因此许多类都会重写(Override)这个方法以返回更有意义的字符串表示。

如果你的 Employee 类没有重写 toString() 方法,那么当你打印 employees[i] 时,你将得到一个类似于 Employee@15db9742 的字符串,其中 Employee 是类名,@ 符号后面跟随的是对象的哈希码的无符号十六进制表示。

方法的重载与重写

  • 重载:方法名相同,形参列表不同。不看返回值类型
  • 重写:父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置覆盖。在程序执行时,子类的方法将覆盖父类的方法。

注:

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表

  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。 注意:如果返回值类型是基本数据类型和void,那么必须是相同

  • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private) 注意:① 父类私有方法(private)不能重写 ② 跨包的父类缺省的方法也不能重写

  • 子类方法抛出的异常不能大于父类被重写方法的异常

  • 此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

案例

public class TestOverloadOverride {
 public static void main(String[] args) {
          Son s=new Son();
        s.method(1);
        Daughter d=new Daughter();
        d.method(1,2);//这里必须要传参吗,如果设置一个函数可传可不传,应该怎么办?
        d.method(1);

    }
}

 class Father{
    public void method(int i){
        System.out.println("Father.method");
    }
}
//儿子对父亲的扩展继承,并且重写方法
 class Son extends Father{
    public void  method(int i){
        System.out.println("Son.Method");
    }
}
//女儿对父亲的扩展继承,并且重载方法
class Daughter extends Father{
    public void  method(int i,int j){
        System.out.println("Daughter.Method");
    }
}

打印结果:

image.png 结论:在继承中,既可以对父类方法进行重写,也可以进行重载,两者互不冲突

练习

  1. 如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗?

不,这种情况不被称为“重写”(Override)。在Java中,方法重写(Overriding)涉及到子类修改或实现从父类继承来的方法的行为。要构成重写,需满足几个条件,其中一条是子类中的方法必须与父类中的某个方法具有相同的签名(方法名、参数列表以及返回类型),并且父类中的方法访问权限不能比子类中的更严格。

当你尝试在子类中将一个父类的 private 方法以 default(即包访问权限)重新声明时,实际上是在子类中定义了一个全新的方法,而不是重写父类的方法。这是因为 private 方法在类的外部以及子类中是不可见的,所以子类并不直接继承父类的 private 方法,也就无法对其进行重写。

子类不能重写父类中声明为private权限修饰的方法,可以理解为,父类中private出了父类,别人就看不见了

super的理解⭐⭐⭐

在Java类中使用super来调用父类中的指定操作:

  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可用于在子类构造器中调用父类的构造器

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

super的使用场景

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

举例

public class Phone {
    public void sendMessage() {

        System.out.println("发短信");
    }

    public void call(){
        System.out.println("打电话");
    }
    public void showNum(){
        System.out.println("来电显示号码");
    }
}

 class SmartPhone extends Phone{
//重写父类来电显示功能的方法
public void showNum(){
    System.out.println("重写了父类来电显示号码");
    //保留父类来电显示号码的功能
    super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}

class run{
    public static void main(String[] args) {
      SmartPhone smartPhone=new SmartPhone();
              smartPhone.showNum();
    }
}

image.png

总结:

  • 方法前面没有super.和this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有super.

    • 从当前子类的直接父类找,如果没有,继续往上追溯

子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

举例

package com.baidu.xiaolong.test;

class Father {
    int a = 10;
    int b = 11;

}

class Son extends Father {
    int a = 20;

    public void test() {
        //子类与父类的属性同名,子类对象中就有两个a
        System.out.println("子类的a:" + a);//20  先找局部变量找,没有再从本类成员变量找
        System.out.println("子类的a:" + this.a);//20   先从本类成员变量找
        System.out.println("父类的a:" + super.a);//10    直接从父类成员变量找

        //子类与父类的属性不同名,是同一个b
        System.out.println("b = " + b);//11  先找局部变量找,没有再从本类成员变量找,没有再从父类找
        System.out.println("b = " + this.b);//11   先从本类成员变量找,没有再从父类找
        System.out.println("b = " + super.b);//11  直接从父类局部变量找
    }

    public void method(int a,int b) {

        //子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
        System.out.println("局部变量的a:" + a);//30  先找局部变量
        System.out.println("子类的a:" + this.a);//20  先从本类成员变量找
        System.out.println("父类的a:" + super.a);//10  直接从父类成员变量找

        System.out.println("b = " + b);//13  先找局部变量
        System.out.println("b = " + this.b);//11  先从本类成员变量找,没有再从父类找
        System.out.println("b = " + super.b);//11  直接从父类局部变量找

    }
}

class Test {

    public static void main(String[] args) {
        Son son = new Son();
        son.test();
        son.method(30,13);
    }
    }

总结:起点不同(就近原则)

  • 变量前面没有super.和this.

    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量
    • 如果不是局部变量,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
  • 变量前面有this.

    • 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
  • 变量前面super.

    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)

特别说明:应该避免子类声明和父类重名的成员变量

子类构造器中调用父类构造器

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。

② 规定:“super(形参列表)”,必须声明在构造器的首行。

③ 我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器, 结合②,结论:在构造器的首行,"this(形参列表)" 和 "super(形参列表)"只能二选一。

④ 如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)", ​ 则子类此构造器默认调用"super()",即调用父类中空参的构造器。

⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。 只能是这两种情况之一。

⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)",则剩下的那个一定使用"super(形参列表)"。

开发中常见错误:

如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错

案例一

class A{

}
class B extends A{

}

class Test{
    public static void main(String[] args){
        B b = new B();
        //A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
        //但是因为都是默认的,没有打印语句,看不出来
    }
}

案例二

class A{
	A(){
		System.out.println("A类无参构造器");
	}
}
class B extends A{

}
class Test{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个无参构造,
		//B类默认有一个无参构造,
		//B类的默认无参构造中会默认调用A类的无参构造
        //可以看到会输出“A类无参构造器"
    }
}

案例三

class A{
	A(){
		System.out.println("A类无参构造器");
	}
}
class B extends A{
	B(){
		System.out.println("B类无参构造器");
	}
}
class Test{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个无参构造,
		//B类显示声明一个无参构造,        
		//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
        //可以看到会输出“A类无参构造器"和"B类无参构造器")
    }
}

案例四

class A{
	A(){
		System.out.println("A类无参构造器");
	}
}
class B extends A{
	B(){
        super();
		System.out.println("B类无参构造器");
	}
}
class Test{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个无参构造,
		//B类显示声明一个无参构造,        
		//B类的无参构造中明确写了super(),表示调用A类的无参构造
        //可以看到会输出“A类无参构造器"和"B类无参构造器")
    }
}

案例五

class A{
	A(int a){
		System.out.println("A类有参构造器");
	}
}
class B extends A{
	B(){
		System.out.println("B类无参构造器");
	}
}
class Test05{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
		//B类显示声明一个无参构造,        
		//B类的无参构造没有写super(...),表示默认调用A类的无参构造
        //编译报错,因为A类没有无参构造
    }
}

案例六

class A{
	A(int a){
		System.out.println("A类有参构造器");
	}
}
class B extends A{
	B(){
		super();
		System.out.println("B类无参构造器");
	}
}
class Test06{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
		//B类显示声明一个无参构造,        
		//B类的无参构造明确写super(),表示调用A类的无参构造
        //编译报错,因为A类没有无参构造
    }
}

案例七

class A{
	A(int a){
		System.out.println("A类有参构造器");
	}
}
class B extends A{
	B(int a){
		super(a);
		System.out.println("B类有参构造器");
	}
}
class Test07{
    public static void main(String[] args){
        B b = new B(10);
        //A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
		//B类显示声明一个有参构造,        
		//B类的有参构造明确写super(a),表示调用A类的有参构造
        //会打印“A类有参构造器"和"B类有参构造器"
    }
}

案例八

class A{
    A(){
        System.out.println("A类无参构造器");
    }
	A(int a){
		System.out.println("A类有参构造器");
	}
}
class B extends A{
    B(){
        super();//可以省略,调用父类的无参构造
        System.out.println("B类无参构造器");
    }
	B(int a){
		super(a);//调用父类有参构造
		System.out.println("B类有参构造器");
	}
}
class Test8{
    public static void main(String[] args){
        B b1 = new B();
        B b2 = new B(10);
    }
}

总结

1、this和super的意义

this:当前对象

  • 在构造器和非静态代码块中,表示正在new的对象
  • 在实例方法中,表示调用当前方法的对象

super:引用父类声明的成员

2、this和super的使用格式

  • this

    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
    • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super

    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

面向对象三大特性之多态性[编译看左,运行看右]⭐⭐⭐

面向对象编程的三大特征:封装、继承、多态

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象

格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)

父类类型 变量名 = 子类对象;

注:多态性只适用于方法,不适用于属性。

为什么需要多态性(polymorphism)?

开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。

案例:

(1)声明一个Dog类,包含public void eat()方法,输出“狗啃骨头”

(2)声明一个Cat类,包含public void eat()方法,输出“猫吃鱼仔”

(3)声明一个Person类,功能如下:

  • 包含宠物属性
  • 包含领养宠物方法 public void adopt(宠物类型Pet)
  • 包含喂宠物吃东西的方法 public void feed(),实现为调用宠物对象.eat()方法
public class Dog {
    public void eat(){
        System.out.println("狗啃骨头");
    }
}
public class Cat {
    public void eat(){
        System.out.println("猫吃鱼仔");
    }
}
public class Person {
    private Dog dog;

    //adopt:领养
    public void adopt(Dog dog){
        this.dog = dog;
    }

    //feed:喂食
    public void feed(){
        if(dog != null){
            dog.eat();
        }
    }
    /*
    问题:
    1、从养狗切换到养猫怎么办?修改代码把Dog修改为养猫?
    2、或者有的人养狗,有的人养猫怎么办?  
    3、要是还有更多其他宠物类型怎么办?
    如果Java不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。
    */
}

解决办法:

package com.baidu.xiaolong.test;

public class AnimalTest {
    public static void main(String[] args) {
        AnimalTest animalTest = new AnimalTest();
        animalTest.adopt(new Dog());
        animalTest.adopt(new Cat());

    }


    /**
     * Animal animal  =new Dog()
     * Animal animal  =new Cat()
     * 这里就体现了JAVA的多态性,左边是父类类型,右边是子类对象
     * 这样当调用方法时,执行的就是子类对象身上的方法
     *
     * @param animal
     */
    public void adopt(Animal animal) {
        animal.eat();
        animal.jump();
    }

}

class Animal {

    public void eat() {
        System.out.println("动物进食");
    }

    public void jump() {
        System.out.println("动物跳");
    }
}


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

    public void jump() {
        System.out.println("狗急跳墙");
    }
}

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

    public void jump() {
        System.out.println("猫跑酷");
    }
}

多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。

Student m = new Student();
m.school = "pku"; 	//合法,Student类有school成员变量
Person e = new Student(); 
e.school = "pku";	//非法,Person类没有school成员变量

// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

开发中:

使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

【开闭原则OCP】

  • 对扩展开放,对修改关闭
  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

虚方法调用(Virtual Method Invocation)

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

Person e = new Student();
e.getInfo();    //调用Student类的getInfo()方法

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

向上转型与向下转型

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过

image.png
  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

如何向上或向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

疑问点

案例一

publc class Animal{
private String type;//动物种类

}
publc class Dog extends Animal{
private String type='Dog',
private void Walk{

}

}
下边这是利用了多态性吗?animals的类是Animal[],但是往里面加了new Dog(),这样为什么可以呢,new Dog中很可能有Animal里没有的属性呀?
main{
private Animal[] animals=new Animal[3]
animals[0]=new Dog()

}

如果反过来呢?假如是这样可以吗?
main{
private Dog[] Dogs=new Dog[3]
Dogs[0]=new animal()

}

回答

首先,让我们纠正代码中的语法错误,并解释你的问题。

public class Animal {  
    private String type; // 动物种类  
  
    public Animal() {  
        // 构造函数(可选)  
    }  
  
    // 可能需要getter和setter来访问private字段  
    public String getType() {  
        return type;  
    }  
  
    public void setType(String type) {  
        this.type = type;  
    }  
}  
  
public class Dog extends Animal {  
    // 注意:通常不需要在子类中重新定义与父类同名的字段,除非有特别的需求  
    // 这里的private String type='Dog'; 实际上隐藏了父类的type字段  
  
    private void walk() { // 方法名后应该有括号  
        // 实现Dog的行走方法  
    }  
  
    // 构造函数,可以初始化Animal中的type字段  
    public Dog() {  
        super(); // 调用父类的构造函数  
        // 如果需要,可以在这里设置Animal中的type  
        setType("Dog"); // 假设Animal类有相应的setter  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Animal[] animals = new Animal[3]; // 创建Animal类型的数组  
        animals[0] = new Dog(); // 这是多态性的一个例子  
  
        // 反过来的情况  
        Dog[] dogs = new Dog[3]; // 创建Dog类型的数组  
        // 下面的代码是错误的,因为Animal不是Dog的子类  
        // dogs[0] = new Animal(); // 编译错误  
    }  
}

在Java中,多态性允许你将子类的对象赋值给父类类型的变量。在你的例子中,animals[0] = new Dog(); 展示了多态性的使用。这里,Dog 是 Animal 的子类,因此你可以将一个 Dog 对象存储在 Animal 类型的数组中。这是合法的,因为 Dog 继承了 Animal,所以 Dog 对象可以被视为 Animal 对象的一个实例(尽管它包含额外的信息)。

当你通过 Animal 类型的引用来访问存储在数组中的 Dog 对象时,你只能访问 Animal 类中定义的属性和方法(除非进行类型转换)。这有助于在不知道具体子类类型的情况下编写通用的代码。

  • 关于反过来的情况

在你的“反过来”的例子中,你试图将一个 Animal 对象存储在 Dog[] 类型的数组中。这是不允许的,因为 Animal 不是 Dog 的子类。在Java中,数组是协变的,但不是逆变的。这意味着如果你有一个 Dog[] 类型的数组,你不能在其中存储 Animal 类型的对象(尽管 Dog 是 Animal 的子类)。数组的这种行为是为了类型安全。

如果你想要存储不同类型的动物(包括狗和其他动物),你应该使用 Animal[] 类型的数组,并在需要时通过类型转换或 instanceof 检查来处理特定类型的动物。

static关键字⭐⭐⭐

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

如果想让一个成员变量被类的所有实例所共享,就用static修饰即可,称为类变量(或类属性)!

静态变量的特点

  • 静态变量的默认值规则和实例变量一样。
  • 静态变量值是所有对象共享。
  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。
  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。
  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。

静态方法的特点

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。
  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。如果有重名问题,使用“类名.”进行区别。

代码块⭐⭐⭐

如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑代码块(或初始化块)。

  • 代码块(或初始化块)的作用

  • 对Java类或对象进行初始化

  • 代码块(或初始化块)的分类

    • 一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block)
    • 没有使用static修饰的,为非静态代码块。

静态代码块

在代码块的前面加static,就是静态代码块。

【修饰符】 class 类{
	static{
        静态代码块
    }
}

静态代码块的特点

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
  4. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
  5. 静态代码块的执行要先于非静态代码块。
  6. 静态代码块随着类的加载而加载,且只执行一次。

非静态代码块

【修饰符】 class 类{
    {
        非静态代码块
    }
    【修饰符】 构造器名(){
    	// 实例初始化代码
    }
    【修饰符】 构造器名(参数列表){
        // 实例初始化代码
    }
}

如果多个重载的构造器有公共代码,并且这些代码都是先于构造器其他代码执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。

非静态代码块的特点

  1. 可以有输出语句。
  2. 可以对类的属性、类的声明进行初始化操作。
  3. 除了调用非静态的结构外,还可以调用静态的变量或方法。
  4. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
  5. 每次创建对象的时候,都会执行一次。且先于构造器执行。

案例

(1)声明User类,

  • 包含属性:username(String类型),password(String类型),registrationTime(long类型),私有化

  • 包含get/set方法,其中registrationTime没有set方法

  • 包含无参构造,

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username就默认为当前系统时间值,
    • password默认为“123456”
  • 包含有参构造(String username, String password),

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username和password由参数赋值
  • 包含public String getInfo()方法,返回:“用户名:xx,密码:xx,注册时间:xx”

(2)编写测试类,测试类main方法的代码如下:

public static void main(String[] args) {
        User u1 = new User();
        System.out.println(u1.getInfo());

        User u2 = new User("song","8888");
        System.out.println(u2.getInfo());
    }

如果不用非静态代码块,User类是这样的:

package com.atguigu.block.no;

public class User {
    private String username;
    private String password;
    private long registrationTime;

    public User() {
        System.out.println("新用户注册");
        registrationTime = System.currentTimeMillis();
        username = registrationTime+"";
        password = "123456";
    }

    public User(String username,String password) {
        System.out.println("新用户注册");
        registrationTime = System.currentTimeMillis();
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public long getRegistrationTime() {
        return registrationTime;
    }
    public String getInfo(){
        return "用户名:" + username + ",密码:" + password + ",注册时间:" + registrationTime;
    }
}

如果提取构造器公共代码到非静态代码块,User类是这样的:

package com.atguigu.block.use;

public class User {
    private String username;
    private String password;
    private long registrationTime;

    {
        System.out.println("新用户注册");
        registrationTime = System.currentTimeMillis();
    }

    public User() {
        username = registrationTime+"";
        password = "123456";
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public long getRegistrationTime() {
        return registrationTime;
    }
    public String getInfo(){
        return "用户名:" + username + ",密码:" + password + ",注册时间:" + registrationTime;
    }
}

final关键字⭐⭐

final修饰类

表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。

例如:String类、System类、StringBuffer类

final class Eunuch{//太监类
    
}
class Son extends Eunuch{//错误
    
}

final修饰方法

表示这个方法不能被子类重写。

例如:Object类中的getClass()

class Father{
	public final void method(){
		System.out.println("father");
	}
}
class Son extends Father{
	public void method(){//错误
		System.out.println("son");
	}
}

final修饰变量

final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。

例如:final double MY_PI = 3.14;

抽象类与抽象方法(或abstract关键字)

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

我们声明一些几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类

  • 抽象类:被abstract修饰的类。
  • 抽象方法:被abstract修饰没有方法体的方法。

举例

public abstract class Animal {
    public abstract void eat();
}
public class Cat extends Animal {
    public void eat (){
        System.out.println("小猫吃鱼和猫粮"); 
    }
}
public class CatTest {
     public static void main(String[] args) {
        // 创建子类对象
        Cat c = new Cat(); 
       
        // 调用eat方法
        c.eat();
    }
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

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

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

    抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

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

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

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

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

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

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

注意事项

  • 不能用abstract修饰变量、代码块、构造器;
  • 不能用abstract修饰私有方法、静态方法、final的方法、final的类。

接口⭐⭐⭐

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

引用数据类型:数组,类,枚举,接口,注解。

举例

public interface USB3{
    //静态常量
    long MAX_SPEED = 500*1024*1024;//500MB/s

    //抽象方法
    void in();
    void out();

    //默认方法
    default void start(){
        System.out.println("开始");
    }
    default void stop(){
        System.out.println("结束");
    }

    //静态方法
    static void show(){
        System.out.println("USB 3.0可以同步全速地进行读写操作");
    }
}

接口的成员说明

在JDK8.0 之前,接口中只允许出现:

(1)公共的静态的常量:其中public static final可以省略

(2)公共的抽象的方法:其中public abstract可以省略

理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现

在JDK8.0 时,接口中允许声明默认方法静态方法

(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略

(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略

在JDK9.0 时,接口又增加了:

(5)私有方法

除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

接口的使用规则

1、类实现接口(implements)

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

【修饰符】 class 实现类  implements 接口{
	// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
    // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

注意:

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法

  2. 默认方法可以选择保留,也可以重写。

    重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了

  3. 接口中的静态方法不能被继承也不能被重写

2、接口的多实现(implements)

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
	// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
    // 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
  	// 重写接口中默认方法【可选】
}

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

3、接口的多继承(extends)

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

4、接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。

5、使用接口的静态成员 接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

6、使用接口的非静态方法

  • 对于接口的静态方法,直接使用“接口名.”进行调用即可

  • 也只能使用“接口名."进行调用,不能通过实现类的对象进行调用

  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用

  • 接口不能直接创建对象,只能创建实现类的对象

JDK8中相关冲突问题

默认方法冲突问题

(1)类优先原则

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:

定义接口:


package com.atguigu.interfacetype;
​
public interface Friend {
    default void date(){//约会
        System.out.println("吃喝玩乐");
    }
}

定义父类:


package com.atguigu.interfacetype;
​
public class Father {
    public void date(){//约会
        System.out.println("爸爸约吃饭");
    }
}

定义子类:


package com.atguigu.interfacetype;
​
public class Son extends Father implements Friend {
    @Override
    public void date() {
        //(1)不重写默认保留父类的
        //(2)调用父类被重写的
//        super.date();
        //(3)保留父接口的
//        Friend.super.date();
        //(4)完全重写
        System.out.println("跟康师傅学Java");
    }
}

定义测试类:


package com.atguigu.interfacetype;
​
public class TestSon {
    public static void main(String[] args) {
        Son s = new Son();
        s.date();
    }
}

2)接口冲突(左右为难)

1、当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

无论你多难抉择,最终都是要做出选择的。

声明接口:


package com.atguigu.interfacetype;
​
public interface BoyFriend {
    default void date(){//约会
        System.out.println("神秘约会");
    }
}

选择保留其中一个,通过“接口名.super.方法名"的方法选择保留哪个接口的默认方法。


package com.atguigu.interfacetype;
​
public class Girl implements Friend,BoyFriend{
​
    @Override
    public void date() {
        //(1)保留其中一个父接口的
//        Friend.super.date();
//        BoyFriend.super.date();
        //(2)完全重写
        System.out.println("跟康师傅学Java");
    }
​
}

测试类


package com.atguigu.interfacetype;
​
public class TestGirl {
    public static void main(String[] args) {
        Girl girl = new Girl();
        girl.date();
    }
}

2、当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢? 另一个父接口:


package com.atguigu.interfacetype;
​
public interface USB2 {
    //静态常量
    long MAX_SPEED = 60*1024*1024;//60MB/s
​
    //抽象方法
    void in();
    void out();
​
    //默认方法
    public default void start(){
        System.out.println("开始");
    }
    public default void stop(){
        System.out.println("结束");
    }
​
    //静态方法
    public static void show(){
        System.out.println("USB 2.0可以高速地进行读写操作");
    }
}

子接口:


package com.atguigu.interfacetype;
​
public interface USB extends USB2,USB3 {
    @Override
    default void start() {
        System.out.println("Usb.start");
    }
​
    @Override
    default void stop() {
        System.out.println("Usb.stop");
    }
}
​

小贴士:

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

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

常量冲突问题

  • 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
  • 当子类同时实现多个接口,而多个接口存在相同同名常量。

此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。

父类和父接口:


package com.atguigu.interfacetype;
​
public class SuperClass {
    int x = 1;
}

package com.atguigu.interfacetype;
​
public interface SuperInterface {
    int x = 2;
    int y = 2;
}

package com.atguigu.interfacetype;
​
public interface MotherInterface {
    int x = 3;
}

子类:


package com.atguigu.interfacetype;
​
public class SubClass extends SuperClass implements SuperInterface,MotherInterface {
    public void method(){
//        System.out.println("x = " + x);//模糊不清
        System.out.println("super.x = " + super.x);
        System.out.println("SuperInterface.x = " + SuperInterface.x);
        System.out.println("MotherInterface.x = " + MotherInterface.x);
        System.out.println("y = " + y);//没有重名问题,可以直接访问
    }
}

接口的总结与面试题

  • 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。

  • 声明接口用interface,接口的成员声明有限制:

    • (1)公共的静态常量
    • (2)公共的抽象方法
    • (3)公共的默认方法(JDK8.0 及以上)
    • (4)公共的静态方法(JDK8.0 及以上)
    • (5)私有方法(JDK9.0 及以上)
  • 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。

  • 接口可以继承接口,关键字是extends,而且支持多继承。

  • 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。

  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

面试题

1、为什么接口中只能声明公共的静态的常量?

因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。

例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA

​ USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA

例如:尚硅谷学生行为规范中规定学员,早上8:25之前进班,晚上21:30之后离开等等。

2、为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。

静态方法:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。

默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。

3、为什么JDK1.9要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。

私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。

抽象类和接口的区别

image.png

在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。

内部类⭐

将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass),类B则称为外部类(OuterClass)

具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。

总的来说,遵循高内聚、低耦合的面向对象开发原则。

image.png

成员内部类

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类

[修饰符] class 外部类{
    [其他修饰符] [static] class 内部类{
    }
}

成员内部类的使用特征,概括来讲有如下两种角色:

  • 成员内部类作为类的成员的角色

    • 和外部类不同,Inner class还可以声明为private或protected;
    • 可以调用外部类的结构。(注意:在静态内部类中不能使用外部类的非静态成员)
    • Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量;
  • 成员内部类作为类的角色

    • 可以在内部定义属性、方法、构造器等结构
    • 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
    • 可以声明为abstract类 ,因此可以被其它的内部类继承
    • 可以声明为final的,表示不能被继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)
  1. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式
  2. 成员内部类可以直接使用外部类的所有成员,包括私有的数据
  3. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的
  • 实例化静态内部类
外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();
  • 实例化非静态内部类
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();

局部内部类

非匿名局部内部类

语法格式:


[修饰符] class 外部类{
    [修饰符] 返回值类型  方法名(形参列表){
            [final/abstract] class 内部类{
        }
    }    
}
  • 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。

    • 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法

匿名局部内部类

因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

枚举类⭐⭐⭐

  • 枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。

  • 枚举类的例子举不胜举:

    • 星期:Monday(星期一)......Sunday(星期天)
    • 性别:Man(男)、Woman(女)
    • 月份:January(1月)......December(12月)
    • 季节:Spring(春节)......Winter(冬天)
    • 三原色:red(红色)、green(绿色)、blue(蓝色)
    • 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
    • 就职状态:Busy(忙碌)、Free(空闲)、Vocation(休假)、Dimission(离职)
    • 订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消)
    • 线程状态:创建、就绪、运行、阻塞、死亡

包装类⭐⭐⭐

Java提供了两个类型系统,基本数据类型引用数据类型。使用基本数据类型在于效率,然而当要使用只针对对象设计的API或新特性(例如泛型),怎么办呢?例如:

//情况1:方法形参
Object类的equals(Object obj)

//情况2:方法形参
ArrayList类的add(Object obj)
//没有如下的方法:
add(int number)
add(double d)
add(boolean b)

//情况3:泛型
Set<T>
List<T>
Cllection<T>
Map<K,V>

Java针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。

包装类的核心解释就是通过包装,使得基本数据类型具备了类的特征,比如封装性、继承性、多态性及各种API的方法【基本数据类型自身比较菜,通过包装可以使其变得强大】

image.png 封装以后的,内存结构对比:
public static void main(String[] args){
	int num = 520;
	Integer obj = new Integer(520);
}

image.png

自定义包装类

public class MyInteger {
    int value;

    public MyInteger() {
    }

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

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

基本数据类型<==>包装类

为什么要掌握两者之间的转换呢?

  • 有些场景下,需要使用基本数据类型对应的包装类的对象,此时就需要将基本数据类型的变量转换为包装类的对象,比如ArrayList这个类中的方法add(Object obj)中需要的实参是obj,此时基本数据类型肯定不行,要把他转换成包装类对象形式
  • 对于包装类,既然我们使用的是对象,由于对象是不能加减乘除的,因此我们需要把这些包装类的对象转换成基本数据类型

装箱:把基本数据类型转为包装类对象

转为包装类的对象,是为了使用专门为对象设计的API和特性

基本数值---->包装对象

Integer obj1 = new Integer(4);//使用构造函数函数
Float f = new Float(“4.56”);
Long l = new Long(“asdf”);  //NumberFormatException

Integer obj2 = Integer.valueOf(4);//使用包装类中的valueOf方法

拆箱:把包装类对象拆为基本数据类型

转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等

包装对象---->基本数值

Integer obj = new Integer(4);
int num1 = obj.intValue();

自动装箱与拆箱:

由于我们经常要做基本类型与包装类之间的转换,从JDK5.0开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

Integer i = 1;
Double d = 1;//错误的,1是int类型

注解⭐⭐⭐

注解(Annotation)是从JDK5.0开始引入,以“@注解名”在代码中存在。例如:

@Override

@Deprecated

@SuppressWarnings(value=”unchecked”)

Annotation 可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。还可以添加一些参数值,这些信息被保存在 Annotation 的 “name=value” 对中。

注解可以在类编译、运行时进行加载,体现不同的功能。

注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。

  • 对于单行注释和多行注释是给程序员看的。
  • 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。

在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码XML配置等。

未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,Struts2有一部分也是基于注解的了。注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式

常见的Annotation作用

@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
package com.annotation.javadoc;
/**
 * @author 尚硅谷-宋红康
 * @version 1.0
 * @see Math.java
 */
public class JavadocTest {
	/**
	 * 程序的主方法,程序的入口
	 * @param args String[] 命令行参数
	 */
	public static void main(String[] args) {
	}
	
	/**
	 * 求圆面积的方法
	 * @param radius double 半径值
	 * @return double 圆的面积
	 */
	public static double getArea(double radius){
		return Math.PI * radius * radius;
	}
}

三个最基本的注解

@Override

  • 用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
  • 只能标记在方法上。
  • 它会被编译器程序读取。

@Deprecated

  • 用于表示被标记的数据已经过时,不推荐使用。
  • 可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
  • 它会被编译器程序读取。

@SuppressWarnings

  • 抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
  • 可以用于修饰类、属性、方法、构造、局部变量、参数
  • 它会被编译器程序读取。

  • 可以指定的警告类型有(了解)

    • all,抑制所有警告
    • unchecked,抑制与未检查的作业相关的警告
    • unused,抑制与未用的程式码及停用的程式码相关的警告
    • deprecation,抑制与淘汰的相关警告
    • nls,抑制与非 nls 字串文字相关的警告
    • null,抑制与空值分析相关的警告
    • rawtypes,抑制与使用 raw 类型相关的警告
    • static-access,抑制与静态存取不正确相关的警告
    • static-method,抑制与可能宣告为 static 的方法相关的警告
    • super,抑制与置换方法相关但不含 super 呼叫的警告
    • ...

元注解

JDK1.5在java.lang.annotation包定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。

(1) @Target: 用于描述注解的使用范围

  • 可以通过枚举类型ElementType的10个常量对象来指定
  • TYPE,METHOD,CONSTRUCTOR,PACKAGE.....

(2) @Retention: 用于描述注解的生命周期

  • 可以通过枚举类型RetentionPolicy的3个常量对象来指定
  • SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
  • 唯有RUNTIME阶段才能被反射读取到

(3) @Documented:表明这个注解应该被 javadoc工具记录。

(4) @Inherited: 允许子类继承父类中的注解

自定义注解

异常处理⭐⭐⭐

Java中是如何表示不同的异常情况,又是如何让程序员得知,并处理异常的呢?

Java中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw)。然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。

运行下面的程序,程序会产生一个数组角标越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生和抛出的过程。

public class ArrayTools {
    // 对给定的数组通过给定的角标获取元素。
    public static int getElement(int[] arr, int index) {
        int element = arr[index];
        return element;
    }
}
public class ExceptionDemo {
    public static void main(String[] args) {
        int[] arr = { 34, 12, 67 };
        intnum = ArrayTools.getElement(arr, 4)
        System.out.println("num=" + num);
        System.out.println("over");
    }
}
<img src="https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0df8c98166e24ac5b845e9ee1573f888~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LiA5Liq6KKr5Luj56CB6IC96K-v55qE5Y6o5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1772890887&x-signature=3K939uvZZi9%2Fib5WyhR8O0JWCHU%3D" alt="image.png" width="100%" />

如何对待异常

对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、以及异常的处理,保证代码的健壮性

Error 和 Exception

Throwable可分为两类:Error和Exception。分别对应着java.lang.Errorjava.lang.Exception两个类。

Error: Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。

  • 例如:StackOverflowError(栈内存溢出)和OutOfMemoryError(堆内存溢出,简称OOM)。

Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。例如:

  • 空指针访问
  • 试图读取不存在的文件
  • 网络连接中断
  • 数组角标越界

编译时异常和运行时异常

Java程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。

image.png

因此,根据异常可能出现的阶段,可以将异常分为:

  • 编译时期异常(即checked异常、受检异常):在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)xx异常,并明确督促程序员提前编写处理它的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。

  • 运行时期异常(即runtime异常、unchecked异常、非受检异常):在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。

    • java.lang.RuntimeException类及它的子类都是运行时异常。比如:ArrayIndexOutOfBoundsException数组下标越界异常,ClassCastException类型转换异常。

异常的处理

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0数据为空输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长臃肿可读性差,程序员需要花很大的精力“堵漏洞”。因此采用异常处理机制。

  • 方式一:try-catch-finally

  • 方式二:throws + 异常类型

方式1:捕获异常(try-catch-finally)

Java提供了异常处理的抓抛模型

  • 前面提到,Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常
  • 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常
  • 如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。
try{
	......	//可能产生异常的代码
}
catch( 异常类型1 e ){
	......	//当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
	...... 	//当产生异常类型2型异常时的处置措施
}  
finally{
	...... //无论是否发生异常,都无条件执行的语句
}

方式2:声明抛出异常类型(throws)

  • 如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通过,但是在当前方法体中可能不适合处理无法给出合理的处理方式,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
  • 具体方式:在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

声明异常格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }  

在throws后面可以写多个异常类型,用逗号隔开。

两种异常处理方式的选择

前提:对于异常,使用相应的处理方式。此时的异常,主要指的是编译时异常。

  • 如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。
  • 如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。
  • 开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。

自定义异常

Java中不同的异常类,分别表示着某一种具体的异常情况。那么在开发中总是有些异常情况是核心类库中没有定义好的,此时我们需要根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题,某员工已在团队中等。

如何自定义异常类

(1)要继承一个异常类型

​ 自定义一个编译时异常类型:自定义类继承java.lang.Exception

​ 自定义一个运行时异常类型:自定义类继承java.lang.RuntimeException

(2)建议大家提供至少两个构造器,一个是无参构造,一个是(String message)构造器。

(3)自定义异常需要提供serialVersionUID

  • 自定义的异常只能通过throw抛出。
  • 自定义异常最重要的是异常类的名字和message属性。当异常出现时,可以根据名字判断异常类型。比如:TeamException("成员已满,无法添加");TeamException("该员工已是某团队成员");
  • 自定义异常对象只能手动抛出。抛出后由try..catch处理,也可以甩锅throws给调用者处理。
image.png

多线程⭐⭐

程序、进程与线程

  • 程序(program) :为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

  • 进程(process) :程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的QQ,运行中的网易音乐播放器。

    • 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
    • 程序是静态的,进程是动态的
    • 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
    • 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。
  • 线程(thread) :进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。

    • 一个进程同一时间若并行执行多个线程,就是支持多线程的。

image.png

  • 线程作为CPU调度和执行的最小单位
  • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
  • 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
image.png

注意:

不同的进程之间是不共享内存的。

进程之间的数据交换和通信的成本很高。

查看进程和线程

image.png image.png image.png

线程调度

image.png

多线程程序的优点

背景: 以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

单核CPU和多核CPU

单核CPU,在一个时间单元内,只能执行一个线程的任务。例如,可以把CPU看成是医院的医生诊室,在一定时间内只能给一个病人诊断治疗。所以单核CPU就是,代码经过前面一系列的前导操作(类似于医院挂号,比如有10个窗口挂号),然后到cpu处执行时发现,就只有一个CPU(对应一个医生),大家排队执行。

这时候想要提升系统性能,只有两个办法,要么提升CPU性能(让医生看病快点),要么多加几个CPU(多整几个医生),即为多核的CPU。

问题:多核的效率是单核的倍数吗?譬如4核A53的cpu,性能是单核A53的4倍吗?理论上是,但是实际不可能,至少有两方面的损耗。

  • 一个是多个核心的其他共用资源限制。譬如,4核CPU对应的内存、cache、寄存器并没有同步扩充4倍。这就好像医院一样,1个医生换4个医生,但是做B超检查的还是一台机器,性能瓶颈就从医生转到B超检查了。
  • 另一个是多核CPU之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到2倍。

并行与并发

并行(parallel) :指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令多个CPU同时执行。比如:多个人同时做不同的事。

并发(concurrency) :指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令单个CPU快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。

线程的创建方式

Java多线程详解——一篇文章搞懂Java多线程 - brokyz - 博客园 (cnblogs.com)

概述

  • Java语言的JVM允许程序运行多个线程,使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。

  • Thread类的特性

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
    • 要想实现多线程,必须在主线程中创建新的线程对象。

方式一:继承Thread类

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

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

代码如下:

package com.thread.thread;
public class ThreadTest {
    public static void main(String[] args) {
    //每new一下,就创建了一个线程
        Print print1=new Print("线程1");
        Print print2=new Print("线程2");
        print1.start();
        print2.start();
        System.out.println("主线程正在执行中");
    }

}

class Print extends Thread {

    public Print(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.getName()+":正在执行!");
    }
}
image.png

注意:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

方式二:实现Runnable接口

Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target参数来创建Thread对象,该Thread对象才是真正 的线程对象。
  3. 调用线程对象的start()方法,启动线程。调用Runnable接口实现类的run方法。

代码如下:

package com.thread.runnable;

public class RunnableTest {
    public static void main(String[] args) {
        Print print=new Print();
            //每new一下,就相当于创建了一个线程,但是这里的print只new了一次,因此是同一个对象
        new Thread(print,"线程1").start();
        new Thread(print,"线程2").start();

        System.out.println("主线程正在执行");
    }
}
class Print implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程正在执行");
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在run方法里面。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上,所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

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

image.png

变形写法

new Thread("新的线程!"){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}.start();
new Thread(new Runnable(){
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+":" + i);
		}
	}
}).start();

对比两种方式

联系

Thread类实际上也是实现了Runnable接口的类。即:

public class Thread extends Object implements Runnable

区别

  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。

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

  • 避免了单继承的局限性
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

练习

Q:创建两个分线程,让其中一个线程输出1-100之间的偶数,另一个线程输出1-100之间的奇数。

A:要使用继承类的方式来创建两个线程,分别输出1-100之间的偶数和奇数,你需要继承Thread类并重写其run方法。但是,通常更推荐的做法是实现Runnable接口,因为Java不支持多重继承,而实现接口则更加灵活。不过,既然你要求使用继承Thread类的方式,下面是一个示例代码:

public class EvenOddPrinter {  
  
    // 继承Thread类输出偶数的线程  
    static class EvenThread extends Thread {  
        public void run() {  
            for (int i = 2; i <= 100; i += 2) {  
                System.out.println(Thread.currentThread().getName() + ": " + i);  
            }  
        }  
    }  
  
    // 继承Thread类输出奇数的线程  
    static class OddThread extends Thread {  
        public void run() {  
            for (int i = 1; i <= 99; i += 2) {  
                System.out.println(Thread.currentThread().getName() + ": " + i);  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
        // 创建偶数线程  
        Thread evenThread = new EvenThread();  
        evenThread.setName("Even");  
  
        // 创建奇数线程  
        Thread oddThread = new OddThread();  
        oddThread.setName("Odd");  
  
        // 启动线程  
        evenThread.start();  
        oddThread.start();  
  
        // 注意:由于线程调度的不确定性,输出顺序可能不是完全交替的  
    }  
}

在这个例子中,我们定义了两个静态内部类EvenThreadOddThread,它们都继承自Thread类并重写了run方法。EvenThread类中的run方法负责输出1-100之间的偶数,而OddThread类中的run方法负责输出1-100之间的奇数。

main方法中,我们分别创建了这两个类的实例,并设置了它们的线程名称。然后,我们调用start方法来启动这两个线程。由于线程调度的不确定性,输出的顺序可能不是完全交替的,但每个线程都会按照其run方法中的逻辑完成自己的任务。

需要注意的是,尽管这个示例使用了继承Thread类的方式,但在实际开发中,实现Runnable接口通常是更推荐的做法,因为它提供了更好的灵活性和可扩展性。

Thread类的常用结构

  • public Thread() :分配一个新的线程对象。

  • public Thread(String name) :分配一个指定名字的新的线程对象。

  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法

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

  • public void run() :此线程要执行的任务在此处定义代码。

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public String getName() :获取当前线程名称。

  • public void setName(String name):设置该线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类

  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  • void join() :等待该线程终止。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

  • public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。

  • void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。

每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

  • Thread类的三个优先级常量:

    • MAX_PRIORITY(10):最高优先级
    • MIN _PRIORITY (1):最低优先级
    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
  • public final int getPriority() :返回线程优先级
  • public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

多线程的生命周期

JDK1.5之前:5种状态

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

image.png

JDK1.5及之后:6种状态

在java.lang.Thread.State的枚举类中这样定义:

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}
  • NEW(新建):线程刚被创建,但是并未启动。还没调用start方法。

  • RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。

  • Teminated(被终止):表明此线程已经结束生命周期,终止运行。

  • 重点说明,根据Thread.State的定义,阻塞状态分为三种BLOCKEDWAITINGTIMED_WAITING

    • BLOCKED(锁阻塞):在API中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。

      • 比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
    • TIMED_WAITING(计时等待):在API中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

      • 当前线程执行过程中遇到Thread类的sleepjoin,Object类的wait,LockSupport类的park方法,并且在调用这些方法时,设置了时间,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断。
    • WAITING(无限等待):在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

      • 当前线程执行过程中遇到遇到Object类的wait,Thread类的join,LockSupport类的park方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入WAITING状态,直到被唤醒。

        • 通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;
        • 通过Condition的await进入WAITING状态的要有Condition的signal方法唤醒;
        • 通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒
        • 通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;

说明:当从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

image.png

线程安全问题

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

image.png 案例:

火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。

1、局部变量不能共享

package com.baidu.thread.saleTickets;

/**
 * 继承的方式卖票
 */
public class WindowSale {


    public static void main(String[] args) {
        SaleTicket saleTicket1=new SaleTicket();
        SaleTicket saleTicket2=new SaleTicket();
        SaleTicket saleTicket3=new SaleTicket();
        saleTicket1.setName("窗口1");
        saleTicket2.setName("窗口2");
        saleTicket3.setName("窗口3");
        saleTicket1.start();
        saleTicket2.start();
        saleTicket3.start();

    }
}


class SaleTicket extends Thread{


    @Override
    public void run() {
         int nums=100;
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(nums>0){
                System.out.println(Thread.currentThread().getName()+ "当前卖出去的票是"+nums);

            }else{
                break;
            }
            nums--;

        }


    }
}

image.png

结果:发现卖出300张票。

问题:局部变量是每次调用方法都是独立的,那么每个线程的run()的ticket是独立的,不是共享数据。

2、不同对象的实例变量不共享

package com.baidu.thread.saleTickets;

/**
 * 继承的方式卖票
 */
public class WindowSale {


    public static void main(String[] args) {
        SaleTicket saleTicket1=new SaleTicket();
        SaleTicket saleTicket2=new SaleTicket();
        SaleTicket saleTicket3=new SaleTicket();
        saleTicket1.setName("窗口1");
        saleTicket2.setName("窗口2");
        saleTicket3.setName("窗口3");
        saleTicket1.start();
        saleTicket2.start();
        saleTicket3.start();

    }
}


class SaleTicket extends Thread{
    private int nums=100;

    @Override
    public void run() {

        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(nums>0){
                System.out.println(Thread.currentThread().getName()+ "当前卖出去的票是"+nums);

            }else{
                break;
            }
            nums--;

        }


    }
}

结果:发现卖出300张票。

问题:不同的实例对象的实例变量是独立的。

3、不同对象的实例变量不共享

把案例2中的变量用static修饰

private static int nums=100;

image.png

结果:发现卖出近100张票。

问题1:但是有重复票或负数票问题。

原因:线程安全问题

问题2:如果要考虑有两场电影,各卖100张票等

原因:TicketThread类的静态变量,是所有TicketThread类的对象共享

4、同一个对象的实例变量共享

package com.baidu.thread.saleTickets.runnalbe;

public class WindowSale {

    public static void main(String[] args) {
        Sale sale=new Sale();
        Thread t1=new Thread(sale,"窗口1");
        Thread t2=new Thread(sale,"窗口2");
        Thread t3=new Thread(sale,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}



class Sale implements Runnable{
private int nums=100;

    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(nums>0){
                System.out.println(Thread.currentThread().getName()+ "当前卖出去的票是"+nums);

            }else{
                break;
            }
            nums--;

        }
    }
}

image.png

结果:发现卖出近100张票。

问题:但是有重复票或负数票问题。

原因:线程安全问题

5、抽取资源类,共享同一个资源对象

package com.atguigu.unsafe;

//1、编写资源类
class Ticket {
    private int ticket = 100;

    public void sale() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        } else {
            throw new RuntimeException("没有票了");
        }
    }

    public int getTicket() {
        return ticket;
    }
}

public class SaleTicketDemo5 {
    public static void main(String[] args) {
        //2、创建资源对象
        Ticket ticket = new Ticket();

        //3、启动多个线程操作资源类的对象
        Thread t1 = new Thread("窗口一") {
            public void run() {
                while (true) {
                    ticket.sale();
                }
            }
        };
        Thread t2 = new Thread("窗口二") {
            public void run() {
                while (true) {
                    ticket.sale();
                }
            }
        };
        Thread t3 = new Thread(new Runnable() {
            public void run() {
                ticket.sale();
            }
        }, "窗口三");


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

结果:发现卖出近100张票。

问题:但是有重复票或负数票问题。

原因:线程安全问题

线程安全问题解决办法

同步机制解决线程安全问题

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

image.png 根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。因为Java对象在堆中的数据分为分为对象头、实例变量、空白的填充。而对象头中包含:

  • Mark Word:记录了和当前对象有关的GC、锁标记等信息。
  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。
  • 数组长度(只有数组对象才有)

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。

同步代码块和同步方法

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


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

说明:

  1. 操作共享数据(多个线程共同操作的变量)的代码,即为需要被同步的代码。 不能多包涵代码(效率低,如果包到while前面就变成了单线程了),也不能少包含代码
  2. 共享数据:多个线程共同操作的变量。
  3. 同步监视器:俗称,锁。任何一个类的对象都可以充当锁。但是所有的线程都必须共用一把锁,共用一个对象。

锁的选择:

  1. 自行创建,共用对象,如下面demo中的Object对象。

  2. 使用this表示当前类的对象

    继承Thread的方法中的锁不能使用this代替,因为继承thread实现多线程时,会创建多个子类对象来代表多个线程,这个时候this指的时当前这个类的多个对象,不唯一,无法当作锁。

    实现Runnable接口的方式中,this可以当作锁,因为这种方式只需要创建一个实现类的对象,将实现类的对象传递给多个Thread类对象来当作多个线程,this就是这个一个实现类的对象,是唯一的,被所有线程所共用的对象。

  3. 使用类当作锁,以下面demo为例,其中的锁可以写为WindowThread.class, 从这里可以得出结论,类也是一个对象

优点:同步的方式,解决了线程安全的问题

缺点:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

public class SafeTicketsWindow {
    public static void main(String[] args) {
        WindowThread ticketsThread02 = new WindowThread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

class WindowThread implements Runnable {
    private int tiketsNum = 100;
    
    //由于,Runnable实现多线程,所有线程共用一个实现类的对象,所以三个线程都共用实现类中的这个Object类的对象。
    Object obj = new Object();
    //如果时继承Thread类实现多线程,那么需要使用到static Object obj = new Object();
    
    public void run() {
        
        //Object obj = new Object();
        //如果Object对象在run()方法中创建,那么每个线程运行都会生成自己的Object类的对象,并不是三个线程的共享对象,所以并没有给加上锁。
        
        while (true) {
            synchronized (obj) {
                if (tiketsNum > 0) {
                    try {
                        //手动让线程进入阻塞,增大安全性发生的概率
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum + "\t剩余票数:" + --tiketsNum);
                } else {
                    break;
                }
            }
        }
    }
}

同步方法: synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。

要点:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
  2. 非静态的同步方法,同步监视器是:this。
  3. 静态的同步方法,同步监视器是:当前类本身。
public synchronized void method(){
    可能会产生线程安全问题的代码
}

public class Window02 {
    public static void main(String[] args) {
        Window02Thread ticketsThread02 = new Window02Thread();
        Thread t1 = new Thread(ticketsThread02);
        Thread t2 = new Thread(ticketsThread02);
        Thread t3 = new Thread(ticketsThread02);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

class Window02Thread implements Runnable {
    private int tiketsNum = 100;

    @Override
    public void run() {
        while (tiketsNum > 0) {
            show();
        }
    }

    private synchronized void show() { //同步监视器:this
        if (tiketsNum > 0) {
            try {
                //手动让线程进入阻塞,增大安全性发生的概率
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":\t票号:" + tiketsNum + "\t剩余票数:" + --tiketsNum);
        }
    }
}

练习

银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

问题:该程序是否有安全问题,如果有,如何解决?

package com.broky.multiThread.exer;

/**
 * 练习1
 * 银行有一个账户
 * 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
 * 分析:
 * 1.是否有多个线程问题? 是,有两个储户线程。
 * 2.是否有共享数据? 是,两个储户向同一个账户存钱
 * 3.是否有线程安全问题: 有
 *
 * @author 13roky
 * @date 2021-04-22 12:38
 */
public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account();
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("储户1");
        c2.setName("储户2");

        c1.start();
        c2.start();

    }
}

class Account {
    private double accountSum;

    public Account() {
        this.accountSum = 0;
    }

    public Account(double accountSum) {
        this.accountSum = accountSum;
    }

    //存钱
    public void deppsit(double depositNum) {
        synchronized (this) {
            if (depositNum > 0) {
                accountSum = accountSum + depositNum;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ": 存钱成功,当前余额为:\t" + accountSum);
            }
        }

    }

}

class Customer extends Thread {
    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            acct.deppsit(1000);
        }
    }
}

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

image.png

举例1:

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

		StringBuilder s1 = new StringBuilder();
		StringBuilder s2 = new StringBuilder();

		new Thread() {
			public void run() {
				synchronized (s1) {
					s1.append("a");
					s2.append("1");
					
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

					synchronized (s2) {
						s1.append("b");
						s2.append("2");

						System.out.println(s1);
						System.out.println(s2);

					}
				}
			}
		}.start();

		new Thread() {
			public void run() {
				synchronized (s2) {
					s1.append("c");
					s2.append("3");

					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
					synchronized (s1) {
						s1.append("d");
						s2.append("4");

						System.out.println(s1);
						System.out.println(s2);

					}

				}
			}
		}.start();

	}
}

举例2:

class A {
	public synchronized void foo(B b) {
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();
		dl.init();
	}
}

诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

JDK5.0新特性:Lock(锁)

  • JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

  • 在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

    • ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
  • Lock锁也称同步锁,加锁与释放锁方法,如下:

    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。
class A{
    //1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
	private final ReentrantLock lock = new ReenTrantLock();
	public void m(){
        //2. 调动lock(),实现需共享的代码的锁定
		lock.lock();
		try{
			//保证线程安全的代码;
		}
		finally{
            //3. 调用unlock(),释放共享代码的锁定
			lock.unlock();  
		}
	}
}

synchronized与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。
  4. (了解)Lock锁可以对读不加锁,对写加锁,synchronized不可以
  5. (了解)Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以

说明:开发建议中处理线程安全问题优先使用顺序为:

• Lock ----> 同步代码块 ----> 同步方法

String⭐⭐⭐

String的特性

  • java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如"hello" )都可以看作是实现此类的实例。
  • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。
  • 字符串String类型本身是final声明的,意味着我们不能继承String。
  • String对象的字符内容是存储在一个字符数组value[]中的。"hello" 等效于 char[] data={'h','e','l','l','o'}
image.png
//jdk8中的String源码:
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[]; //String对象的字符内容是存储在此数组中
 
    /** Cache the hash code for the string */
    private int hash; // Default to 0
  • private意味着外面无法直接获取字符数组,而且String没有提供value的get和set方法。
  • final意味着字符数组的引用不可改变,而且String也没有提供方法来修改value数组某个元素值
  • 因此字符串的字符数组内容也不可变的,即String代表着不可变的字符序列。即,一旦对字符串进行修改,就会产生新对象。
  • JDK9只有,底层使用byte[]数组。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { 
    @Stable
    private final byte[] value;
}

//官方说明:... that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

//细节:... The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.

String的内存结构

因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对象。

JDK6中,字符串常量池在方法区。JDK7开始,就移到堆空间,直到目前JDK17版本。

内存结构之练习

1、String str2 = new String("hello"); 在内存中创建了几个对象?

在Java中,每当使用​​new​​关键字创建一个对象时,会经历以下几个步骤:

  1. 在堆内存中分配空间,创建一个新的对象。
  2. 调用对象的构造方法,对对象进行初始化。
  3. 返回对象的引用,赋值给指定的变量。

String类在Java中是一个特殊的类,它有一个特殊的地位和使用方式。当我们使用​​String s = new String()​​语句创建一个String对象时,实际上会创建两个对象。

堆内存中的对象:通过​​new String()​​创建的对象存储在堆内存中。这个对象是一个新的String对象,可以在堆内存中进行操作。这个对象是可变的,可以通过一些方法来修改其内容。 字符串常量池中的对象:Java中有一个特殊的内存区域叫做字符串常量池(String Pool),用于存储字符串常量。当我们使用​​new String()​​创建一个String对象时,首先在堆内存中创建一个新的对象,然后会检查字符串常量池中是否已经存在相同内容的字符串。如果字符串常量池中已经存在该字符串,则不会创建新的对象,而是将已存在的字符串对象的引用赋值给变量​​s​​。

在这个示例代码中,通过​​new String()​​创建了一个新的String对象,这个对象存储在堆内存中。然后,Java会检查字符串常量池中是否已经存在一个空字符串("")。如果字符串常量池中已经存在空字符串,那么这个空字符串对象的引用会被赋值给变量​​s​​。如果字符串常量池中不存在空字符串,那么会在字符串常量池中创建一个新的空字符串对象,并将其引用赋值给变量​​s​​。 因此,通过​​String s = new String()​​语句创建了两个对象:一个在堆内存中,一个在字符串常量池中。不过,实际上我们很少使用这种方式来创建String对象,通常会直接使用字符串字面量来创建String对象,例如​​String s = ""​​或者​​String s = "Hello"​​。这样可以确保只创建一个对象,并且优化了内存使用。

2、intern()

  • String s1 = "a";

说明:在字符串常量池中创建了一个字面量为"a"的字符串。

  • s1 = s1 + "b";

说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+"b"(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。

  • String s2 = "ab";

说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。

  • String s3 = "a" + "b";

说明:s3指向字符串常量池中已经创建的"ab"的字符串。

  • String s4 = s1.intern();

说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4。

结论:

(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。

(2)常量与变量 或 变量与变量:结果在堆中

(3)拼接后调用intern方法:返回值在常量池中

@Test
public void test01(){
	String s1 = "hello";
	String s2 = "world";
	String s3 = "helloworld";
		
	String s4 = s1 + "world";//s4字符串内容也helloworld,s1是变量,"world"常量,变量 + 常量的结果在堆中
	String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是变量,变量 + 变量的结果在堆中
	String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
		
	System.out.println(s3 == s4);//false
	System.out.println(s3 == s5);//false
	System.out.println(s3 == s6);//true
}

@Test
public void test02(){
	final String s1 = "hello";
	final String s2 = "world";
	String s3 = "helloworld";
	
	String s4 = s1 + "world";//s4字符串内容也helloworld,s1是常量,"world"常量,常量+常量结果在常量池中
	String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是常量,常量+ 常量 结果在常量池中
	String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
		
	System.out.println(s3 == s4);//true
	System.out.println(s3 == s5);//<img src="true" alt="" width="100%" />
	System.out.println(s3 == s6);//true
}

@Test
public void test01(){
	String s1 = "hello";
	String s2 = "world";
	String s3 = "helloworld";
		
	String s4 = (s1 + "world").intern();//把拼接的结果放到常量池中
	String s5 = (s1 + s2).intern();
		
	System.out.println(s3 == s4);//true
	System.out.println(s3 == s5);//true
}

String的常用API

  • public String() :初始化新创建的 String对象,以使其表示空字符序列。
  • String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
  • public String(char[] value) :通过当前参数中的字符数组来构造新的String。
  • public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的String。
  • public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。
  • public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的String。

字符串 --> 基本数据类型、包装类:

  • Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
  • 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。

基本数据类型、包装类 --> 字符串:

  • 调用String类的public String valueOf(int n)可将int型转换为字符串
  • 相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换。

字符数组 --> 字符串:

  • String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。

字符串 --> 字符数组:

  • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
  • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

字符串 --> 字节数组:(编码)

  • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
  • public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

字节数组 --> 字符串:(解码)

  • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
  • String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
  • String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。

常用方法

(1)boolean isEmpty():字符串是否为空

(2)int length():返回字符串的长度

(3)String concat(xx):拼接

(4)boolean equals(Object obj):比较字符串是否相等,区分大小写

(5)boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写

(6)int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小

(7)int compareToIgnoreCase(String other):比较字符串大小,不区分大小写

(8)String toLowerCase():将字符串中大写字母转为小写

(9)String toUpperCase():将字符串中小写字母转为大写

(10)String trim():去掉字符串前后空白符

(11)public String intern():结果在常量池中共享

查找

(12)boolean contains(xx):是否包含xx

(13)int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1

(14)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

(15)int lastIndexOf(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回-1

(16)int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。

字符串截取 (17)String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。

(18)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。

和字符/字符数组相关

(19)char charAt(index):返回[index]位置的字符

(20)char[] toCharArray(): 将此字符串转换为一个新的字符数组返回

(21)static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String

(22)static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String

(23)static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String

(24)static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String

开头与结尾

(25)boolean startsWith(xx):测试此字符串是否以指定的前缀开始

(26)boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始

(27)boolean endsWith(xx):测试此字符串是否以指定的后缀结束

替换

(28)String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。

(29)String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

(30)String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

(31)String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

字符串相关类之可变字符序列:StringBuffer、StringBuilder

因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK又在java.lang包提供了可变字符序列StringBuffer和StringBuilder类型。

java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。比如:

//情况1:
String s = new String("我喜欢学习"); 
//情况2:
StringBuffer buffer = new StringBuffer("我喜欢学习"); 
buffer.append("数学"); 
image.png image.png
  • StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样。

  • 区分String、StringBuffer、StringBuilder

    • String:不可变的字符序列; 底层使用char[]数组存储(JDK8.0中)
    • StringBuffer:可变的字符序列;线程安全(方法有synchronized修饰),效率低;底层使用char[]数组存储 (JDK8.0中)
    • StringBuilder:可变的字符序列; jdk1.5引入,线程不安全的,效率高;底层使用char[]数组存储(JDK8.0中)

JDK8:新的日期时间API

如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:

  • 可变性:像日期和时间这样的类应该是不可变的。
  • 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
  • 格式化:格式化只对Date有用,Calendar则不行。
  • 此外,它们也不是线程安全的;不能处理闰秒等。

闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒); 闰秒一般加在公历年末或公历六月末。

目前,全球已经进行了27次闰秒,均为正闰秒。 总结:对日期和时间的操作一直是Java程序员最痛苦的地方之一

第三次引入的API是成功的,并且Java 8中引入的java.time API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。

Java 8 以一个新的开始为 Java 创建优秀的 API。新的日期时间API包含:

  • java.time – 包含值对象的基础包
  • java.time.chrono – 提供对不同的日历系统的访问。
  • java.time.format – 格式化和解析时间和日期
  • java.time.temporal – 包括底层框架和扩展特性
  • java.time.zone – 包含时区支持的类

说明:新的 java.time 中包含了所有关于时钟(Clock),本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。

尽管有68个新的公开类型,但是大多数开发者只会用到基础包和format包,大概占总数的三分之一。

本地日期时间:LocalDate、LocalTime、LocalDateTime

image.png
import org.junit.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class TestLocalDateTime {
    @Test
    public void test01(){
        LocalDate now = LocalDate.now();
        System.out.println(now);
    }
    @Test
    public void test02(){
        LocalTime now = LocalTime.now();
        System.out.println(now);
    }
    @Test
    public void test03(){
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);
    }
    @Test
    public void test04(){
        LocalDate lai = LocalDate.of(2019, 5, 13);
        System.out.println(lai);
    }
	@Test
    public void test05(){
        LocalDate lai = LocalDate.of(2019, 5, 13);
        System.out.println(lai.getDayOfYear());
    }
	@Test
    public void test06(){
        LocalDate lai = LocalDate.of(2019, 5, 13);
        LocalDate go = lai.plusDays(160);
        System.out.println(go);//2019-10-20
    }
    @Test
    public void test7(){
        LocalDate now = LocalDate.now();
        LocalDate before = now.minusDays(100);
        System.out.println(before);//2019-02-26
    }   
}

瞬时:Instant

  • Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。

    • 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
  • java.time.Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。

image.png

日期时间格式化:DateTimeFormatter

该类提供了三种格式化方法:

  • (了解)预定义的标准格式。如:ISO_LOCAL_DATE_TIME、ISO_LOCAL_DATE、ISO_LOCAL_TIME
  • (了解)本地化相关的格式。如:ofLocalizedDate(FormatStyle.LONG)
import org.junit.Test;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class TestDatetimeFormatter {
    @Test
    public void test1(){
        // 方式一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        // 格式化:日期-->字符串
        LocalDateTime localDateTime = LocalDateTime.now();
        String str1 = formatter.format(localDateTime);
        System.out.println(localDateTime);
        System.out.println(str1);//2022-12-04T21:02:14.808

        // 解析:字符串 -->日期
        TemporalAccessor parse = formatter.parse("2022-12-04T21:02:14.808");
        LocalDateTime dateTime = LocalDateTime.from(parse);
        System.out.println(dateTime);
    }

    @Test
    public void test2(){
        LocalDateTime localDateTime = LocalDateTime.now();
        // 方式二:
        // 本地化相关的格式。如:ofLocalizedDateTime()
        // FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime
        DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
        
        // 格式化
        String str2 = formatter1.format(localDateTime);
        System.out.println(str2);// 2022年12月4日 下午09时03分55秒

        // 本地化相关的格式。如:ofLocalizedDate()
        // FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate
        DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
        // 格式化
        String str3 = formatter2.format(LocalDate.now());
        System.out.println(str3);// 2022年12月4日 星期日
    }

    @Test
    public void test3(){
        //方式三:自定义的方式(关注、重点)
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        //格式化
        String strDateTime = dateTimeFormatter.format(LocalDateTime.now());
        System.out.println(strDateTime); //2022/12/04 21:05:42
        //解析
        TemporalAccessor accessor = dateTimeFormatter.parse("2022/12/04 21:05:42");
        LocalDateTime localDateTime = LocalDateTime.from(accessor);
        System.out.println(localDateTime); //2022-12-04T21:05:42
    }
}

Java比较器

我们知道基本数据类型的数据(除boolean类型外)需要比较大小的话,之间使用比较运算符即可,但是引用数据类型是不能直接使用比较运算符来比较大小的,因为存的是一个地址。那么,如何解决这个问题呢?

image.png

其实比较对象,就是拿对象里的属性来进行比较,主要有两种排序:自然排序和定制排序

自然排序:java.lang.Comparable

  • Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。

  • 实现 Comparable 的类必须实现 compareTo(Object obj)方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。

  • 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

  • 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与 equals 一致

  • Comparable 的典型实现:(默认都是从小到大排列的)

    • String:按照字符串中字符的Unicode值进行比较
    • Character:按照字符的Unicode值来进行比较
    • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • Date、Time等:后面的日期时间比前面的日期时间大

举例

1、基本数据类型的案例

public class compareTest {
    @Test
     public void test(){
        String[] arr=new String[]{"BBB","AAA","DDD","CCC"};
        Arrays.sort(arr);
        //排序后进行遍历
        for (int i = 0; i <arr.length ; i++) {
            System.out.println(arr[i]);
        }

    }

}

image.png

由于数组里面是基本数据类型,所以可以调用 Arrays.sort这个方法,但是如果是引用数据类型,肯定是报错的

2、引用数据类型的案例 该案例为定义一个数组对象,然后根据其价值进行比较

class Product {
    private String name;
    private int price;

    public Product() {
    }

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + ''' +
                ", price=" + price +
                '}';
    }
}
-----------------------------

 public void test2() {
        Product[] arr = new Product[3];
        arr[0] = new Product("xiaomi", 1999);
        arr[2] = new Product("iphone", 3999);
        arr[1] = new Product("huawei", 2999);
        Arrays.sort(arr);
//       排序后进行遍历
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }

上方代码会报错,因为对于引用对象,不可直接用sort方法进行比较,况且这个方法也不知道需要根据什么属性来进行比较

image.png

因此我们需要重写Product这个类身上的一个方法,具体代码如下:

class Product implements Comparable {
    private String name;
    private int price;

    public Product() {
    }

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + ''' +
                ", price=" + price +
                '}';
    }


    /**
     *  当前的类需要实现Comparable接口中的抽象方法:compareTo(Object o)
     *   该方法中指明了如何判断类的对象的大小,比如按照价格进行比较
     *   如果返回值是正数,则当前对象大
     *   如果返回值的负数,则当前对象小
     *   如果返回值是0,则一样大
     */

    @Override
    public int compareTo(Object o) {
        if(o==this){
            //代表对象的地址一样,说明是同一个对象
            return 0;
        }
        if(o instanceof Product){
            Product p=(Product)o;
           return  Double.compare(this.price,p.price);
        }
        //手动抛异常
        throw new RuntimeException("类型错误");
    }
}
image.png

定制排序:java.util.Comparator

  • 思考

    • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码(例如:一些第三方的类,你只有.class文件,没有源文件)
    • 如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?
  • JDK在设计类库之初,也考虑到这种情况,所以又增加了一个java.util.Comparator接口。强行对多个对象进行整体排序的比较。

    • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
    • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。
package com.atguigu.api;

import java.util.Comparator;
//定义定制比较器类
public class StudentScoreComparator implements Comparator { 
    @Override
    public int compare(Object o1, Object o2) {
        Student s1 = (Student) o1;
        Student s2 = (Student) o2;
        int result = s1.getScore() - s2.getScore();
        return result != 0 ? result : s1.getId() - s2.getId();
    }
}

和数学相关的类

java.lang.Math

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

  • public static double abs(double a) :返回 double 值的绝对值。
double d1 = Math.abs(-5); //d1的值为5
double d2 = Math.abs(5); //d2的值为5

public static double ceil(double a) :返回大于等于参数的最小的整数。

double d1 = Math.ceil(3.3); //d1的值为 4.0
double d2 = Math.ceil(-3.3); //d2的值为 -3.0
double d3 = Math.ceil(5.1); //d3的值为 6.0

public static double floor(double a) :返回小于等于参数最大的整数。

double d1 = Math.floor(3.3); //d1的值为3.0
double d2 = Math.floor(-3.3); //d2的值为-4.0
double d3 = Math.floor(5.1); //d3的值为 5.0

public static long round(double a) :返回最接近参数的 long。(相当于四舍五入方法)

long d1 = Math.round(5.5); //d1的值为6
long d2 = Math.round(5.4); //d2的值为5
long d3 = Math.round(-3.3); //d3的值为-3
long d4 = Math.round(-3.8); //d4的值为-4
  • public static double pow(double a,double b):返回a的b幂次方法
  • public static double sqrt(double a):返回a的平方根
  • public static double random():返回[0,1)的随机值
  • public static final double PI:返回圆周率
  • public static double max(double x, double y):返回x,y中的最大值
  • public static double min(double x, double y):返回x,y中的最小值
  • 其它:acos,asin,atan,cos,sin,tan 三角函数
double result = Math.pow(2,31);
double sqrt = Math.sqrt(256);
double rand = Math.random();
double pi = Math.PI;

Java中的集合框架⭐⭐⭐

注:本章中需要重点掌握list、map、set,在正常业务场景下使用的最多

数组的特点与弊端

  • 数组在内存存储方面的特点

    • 数组初始化以后,长度就确定了。
    • 数组中的添加的元素是依次紧密排列的,有序的,可以重复的。
    • 数组声明的类型,就决定了进行元素初始化时的类型。不是此类型的变量,就不能添加。
    • 可以存储基本数据类型值,也可以存储引用数据类型的变量
  • 数组在存储数据方面的弊端

    • 数组初始化以后,长度就不可变了,不便于扩展
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入、获取元素个数等操作,且效率不高。
    • 数组存储数据的特点单一,只能存储有序的、可以重复的数据
  • Java 集合框架中的类可以用于存储多个对象,还可用于保存具有映射关系的关联数组。

Java集合框架体系

Java 集合可分为 Collection 和 Map 两大体系:

  • Collection接口:用于存储一个一个的数据,也称单列数据集合

    • List子接口:用来存储有序的、可以重复的数据(主要用来替换数组,"动态"数组)

      • 实现类:ArrayList(主要实现类)、LinkedList、Vector
  • Set子接口:用来存储无序的、不可重复的数据(类似于高中讲的"集合")

    • 实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet
  • Map接口:用于存储具有映射关系“key-value对”的集合,即一对一对的数据,也称双列数据集合。(类似于高中的函数、映射。(x1,y1),(x2,y2) ---> y = f(x) )

    • HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties
  • JDK提供的集合API位于java.util包内

image.png image.png image.png

Collection接口及方法

JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)去实现。

这句话的意思是,Java Development Kit(JDK)没有直接提供实现Collection接口的具体类,而是提供了更具体的子接口(例如Set和List),这些子接口有不同的实现类。

让我们详细解释一下:

Collection接口:Collection是Java集合框架的根接口之一,它代表一组对象,这些对象通常称为集合的元素。Collection接口定义了操作集合的基本方法,比如添加元素、删除元素、获取集合大小等。

具体实现类的缺失:JDK本身并没有提供Collection接口的直接实现类。也就是说,你不能直接实例化一个Collection对象,因为Collection是一个接口,而不是一个具体的类。

子接口的存在:相反,JDK提供了多个扩展自Collection接口的子接口,如Set、List、Queue等。

这些子接口分别表示不同的集合类型,具有不同的行为和特性。例如:

  • Set接口表示不允许重复元素的集合。
  • List接口表示允许重复元素且有序的集合。
  • Queue接口表示先进先出(FIFO)的队列集合。

子接口的实现类:每个子接口都有多个具体的实现类,例如HashSet、ArrayList、LinkedList等,它们分别实现了对应的接口,并提供了具体的数据结构和算法来支持集合操作。

因此,上述句子的含义是,Collection接口本身并没有直接的实现类,而是通过其子接口(如Set和List)来提供具体的集合实现。开发者在使用集合时,通常会根据需求选择合适的子接口及其实现类来操作和管理数据。

Collection子接口1:List

  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • JDK API中List接口的实现类常用的有:ArrayListLinkedListVector

1)List接口主要实现类:ArrayList

  • ArrayList 是 List 接口的主要实现类
  • 本质上,ArrayList是对象引用的一个”变长”数组
  • Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合

2)List的实现类之二:LinkedList

对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高。这是由底层采用链表(双向链表)结构存储数据决定的。

image.png

3) List的实现类之三:Vector

  • Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。

  • 在各种List中,最好把ArrayList作为默认选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

  • 特有方法:

    • void addElement(Object obj)
    • void insertElementAt(Object obj,int index)
    • void setElementAt(Object obj,int index)
    • void removeElement(Object obj)
    • void removeAllElements()

Collection子接口2:Set

  • Set接口是Collection的子接口,Set接口相较于Collection接口没有提供额外的方法
  • Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
  • Set集合支持的遍历方式和Collection集合一样:foreach和Iterator。
  • Set的常用实现类有:HashSet、TreeSet、LinkedHashSet。

1)Set主要实现类:HashSet

  • HashSet 是 Set 接口的主要实现类,大多数时候使用 Set 集合时都使用这个实现类。

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存储、查找、删除性能。

  • HashSet 具有以下特点

    • 不能保证元素的排列顺序
    • HashSet 不是线程安全的
    • 集合元素可以是 null
  • HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的哈希值相等,并且两个对象的 equals()方法返回值为true。

  • 对于存放在Set容器中的对象,对应的类一定要重写hashCode()和equals(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

  • HashSet集合中元素的无序性,不等同于随机性。这里的无序性与元素的添加位置有关。具体来说:我们在添加每一个元素到数组中时,具体的存储位置是由元素的hashCode()调用后返回的hash值决定的。导致在数组中每个元素不是依次紧密存放的,表现出一定的无序性。

HashSet中添加元素的过程:

  • 第1步:当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法得到该对象的 hashCode值,然后根据 hashCode值,通过某个散列函数决定该对象在 HashSet 底层数组中的存储位置。

  • 第2步:如果要在数组中存储的位置上没有元素,则直接添加成功。

  • 第3步:如果要在数组中存储的位置上有元素,则继续比较:

    • 如果两个元素的hashCode值不相等,则添加成功;

    • 如果两个元素的hashCode()值相等,则会继续调用equals()方法:

      • 如果equals()方法结果为false,则添加成功。
      • 如果equals()方法结果为true,则添加失败。

2)Set主要实现类:LinkedHashSet

  • LinkedHashSet 是 HashSet 的子类,不允许集合元素重复。
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以添加顺序保存的。
  • LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
image.png

3)Set主要实现类: TreeSet

  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以按照添加的元素的指定的属性的大小顺序进行遍历。

  • TreeSet底层使用红黑树结构存储数据

  • 新增的方法如下: (了解)

    • Comparator comparator()
    • Object first()
    • Object last()
    • Object lower(Object e)
    • Object higher(Object e)
    • SortedSet subSet(fromElement, toElement)
    • SortedSet headSet(toElement)
    • SortedSet tailSet(fromElement)
  • TreeSet特点:不允许重复、实现排序(自然排序或定制排序)

  • TreeSet 两种排序方法:自然排序定制排序。默认情况下,TreeSet 采用自然排序。

    • 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。

      • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
      • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
    • 定制排序:如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。

      • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
      • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象

  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 或compare(Object o1,Object o2)方法比较返回值。返回值为0,则认为两个对象相等。

Map接口

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value

    • Collection集合称为单列集合,元素是孤立存在的(理解为单身)。
    • Map集合称为双列集合,元素是成对存在的(理解为夫妻)。
  • Map 中的 key 和 value 都可以是任何引用类型的数据。但常用String类作为Map的“键”。

  • Map接口的常用实现类:HashMapLinkedHashMapTreeMap`Properties。其中,HashMap是 Map 接口使用频率最高的实现类。