Java 面向对象

类和对象

定义类

面向对象的程序设计过程中有两个重要概念:类(class)和对象(object,也被称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体

Java语言是面向对象的而程序设计语言,类和对象是面向对象的核心

Java语言里定义的简单语法如下:

[修饰符] class 类名
{
    零个到多个构造器定义...
    零个到多个成员变量..
    零个到多个方法..
}

类里各成员之间的定义顺序没有任何影响,各成员之间可以相互调用,但需要指出的是,static修饰的成员不能访问没有static修饰的成员

成员变量用于定义该类或该类的实例所包含的状态数据,方法则用一定义该类或该类的实例的行为特征或者功能实现。构造器用于构造该类的实例,Java语言通过new关键字来调用构造器,从而返回该类的实例

构造器是一个类创建实例的根本途径,如果一个类没有构造器,这个类通常无法创建实例。因此Java语言提供了一个功能:如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器

定义成员变量的语法格式

[修饰符] 类型 成员变量名 [=默认值];
  • 修饰符:修饰符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三个最多只能出现其中之一,可以与static、final组合起来修饰成员变量
  • 类型:Java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型
  • 成员变量名:合法标识符即可,最好由一个或多个有意义的单词连缀而成

定义方法的语法格式

[修饰符] 方法返回值类型 方法名(形参列表)
  • 方法返回值类型:Java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型,如果声明了返回值类型,则方法体内必须有一个有效的return语句,返回一个与返回值类型相匹配的变量或表达式。如果没有返回值,则必须使用void来声明没有返回值
  • 形参列表:定义方法可以接受的参数,形参列表由零组到多组“参数类型 形参名”组成,多组参数以,隔开,形参类型与形参名以空格隔开,一旦方法中定义了参数列表,调用该方法时必须传入对应的参数值

构造器的语法格式

[修饰符] 方法名(形参列表)
{
    // 由零到多条可执行语句组成的构造器执行体
}

构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。如果定义了返回值类型,或者使用void声明构造器没有返回值,则Java会将这个所谓的构造器当成方法类处理——它就不再是构造器

对象的产生和使用

创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实现类

// 通过new关键字调用Person类的构造器,返回一个Person实例
// 将该Person实例赋给p变量
Person p = new Person();

对象、引用和指针

类是一种引用数据类型,因此程序中的Person类型的变量实际上是一个引用,它被存放在栈内存里,指向实际的Person对象;而真正的Person对象则存放在堆内存中

栈内存里的引用变量并未真正存储对象的成员变量,对象的成员变量数据实际存放在堆内存里;而引用变量只是只想该堆内存里的对象。

当一个对象被创建成功以后,这个对象将保存在堆内存中,Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象

对象的this引用

Java提供了一个this关键字,this关键字总是只想调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形

  • 构造器中应用该构造器正在初始化的对象
  • 方法中引用调用该方法的对象

this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量

public class Dog
{
    public void jump()
    {
        System.out.println("正在执行jump方法");
    }
    public void run()
    {
        this.jump();//调用当前实例的jump方法
        //Java对象中的一个成员调用另一个成员可以省略this前缀,因此,也可以改为如下形式
        //jump();
        System.out.println("正在执行run方法");
    }
}

static修饰的方法中不能使用this引用

静态成员不能访问非静态成员

方法

方法时类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。Java里的方法不能独立存在,所有的方法必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

  • 方法不能独立定义,方法只能在类体里定义
  • 从逻辑意义上来看,方法要么属于该类本身,要么属于该类的一个对象
  • 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。

方法参数的传递机制

Java里方法的参数传递方式只有一种:值传递。指将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响

public static void swap(int a,int b)
{
    int tmp = a;
    a = b;
    b = tmp;
    System.out.println("swap方法中 a="+a+",b="+b);
}
public static void main(String[] args)
{
    int a = 5;
    int b = 8;
    swap(a, b);
    System.out.println("交换结束后,a="+a+",b="+b);
}

运行结果

swap方法中 a=8,b=5
交换结束后,a=5,b=8

在main()方法中调用swap()方法时,main()方法还未结束。因此,系统分别为main()方法和swap()方法分配两块栈区,用于保存main()方法和swap()方法的局部变量。main()方法中的a、b变量作为参数传入swap()方法,实际上是在swap()方法栈区中重新产生了两个变量a、b,并将main()方法栈区中a、b变量分别赋给swap()方法战区中的a、b参数

当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋值给方法的形参变量,方法里操作的并不是实际的形参变量

Java对于引用类型的参数传递,一样采用的值传递方式,不过传递的不是对象本身,而是对象的引用,因此在被调用的方法内修改参数引用的对象时,由于引用的是同一个对象,所以原栈区变量引用的对象也会同时修改

形参个数可变

JDK1.5之后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。

如果在定义方法时,在最后一个形参的类型后加三点(...),则表明该形参可以接收多个参数值,多个参数值会被当成数组传入。

public static void test(int a,String... books){
    // books会被当成数组处理
    for(String tmp : books){
        System.out.println("tmp");
    }
}
// 调用test方法
test(3,"测试1","测试2");
// 调用test方法传入数组
test(3,new String[]{"测试1","测试2"});

注意,个数可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个个数可变的形参。个数可变的形参本质就是一个数组类型的形参,因此既可以传入多个参数,也可以传入一个数组

方法重载

Java 允许同一个类定义多个同名方法,如果同一个类中包含了两个或两个以上的方法名相同,但形参列表不同,则被称为重载。

方法重载的要求时两同一不同:同一个类中方法名相同,形参列表不同。

public class Overload {
    public void test(){
        System.out.println("无参数");
    }
    public void test(String msg){
        System.out.println("重载的test方法 " + msg);
    }
    public void test(String... msg){
        System.out.println("形参个数可变的test方法 " + Arrays.toString(msg));
    }
}
public static void main(String[] args) {
    Overload overload = new Overload();
    //当调用方法时,将会根据传入的实参列表匹配
    overload.test();
    overload.test("hello");
    overload.test("h1","h2");
}

输出

无参数
重载的test方法 hello
形参个数可变的test方法 [h1, h2]

如果同时包含了类型相同并且 个数可变的形参和一个形参的重载方法,当传入一个参数时会优先调用一个参数的方法。如果需要调用形参个数可变的方法则需要通过传入数组的方式调用,例如overload.test(new String[]{"hello"});

成员变量 和 局部变量

成员变量指的是在类里定义的变量,局部变量指的是在方法里定义的变量

成员变量被分为静态变量和实例变量两种,定义成员变量时没有static修饰的就是实例变量,有static修饰的就是静态变量。其中静态变量从该类的准备阶段起开始存在,直到系统完全销毁这个类,静态变量的作用域与这个类的生存范围相同;而实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同

局部变量根据定义形式的不同,可以被分为一下三种

  • 形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效
  • 方法局部变量:在方法体内定义的局部变量,它的作用域是从定义该变量的地方生效,到该方法结束时失效
  • 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效

java语法中允许通过实例调用静态成员,但是static修饰的成员属于类本身,不属于实例本身。所以尽量不要使用实例对象去调用静态成员。

构造器

什么是构造器

构造器是一个特殊的方法,这个特殊方法用于创建实例时执行初始化。

public class ConstructorTest {
    public String name;
    public int count;
    // 提供自定义的构造器,该构造器需要两个参数
    public ConstructorTest(String name, int count){
        // 初始化成员变量
        this.name = name;
        this.count = count;
    }
    public static void main(String[] args) {
        //使用自定义构造器来创建对象
        ConstructorTest tc = new ConstructorTest("张三",20);
        System.out.println(tc.name);
        System.out.println(tc.count);
    }
}

如果程序员没有为Java类提供任何构造器,则系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。无论如何,Java类至少包含一个构造器

一旦程序员提供了自定义的构造器,系统就不再提供默认的构造器

构造器重载

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载

public class ConstructorTest {
    public String name;
    public int count;
    public double price;

    // 两个参数构造器
    public ConstructorTest(String name, int count){
        this.name = name;
        this.count = count;
    }

    // 三个参数构造器
    public ConstructorTest(String name, int count,double price){
        // 通过this调用另一个构造器的初始化代码
        this(name,count);
        this.price = price;
    }
}

使用this可以调用另一个重载的构造器只能在构造器,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。

访问修饰符

  • private(当前类访问权限):只能在当前类的内部被访问

  • default(包访问权限):如果一个类里的成员或者一个外部类不适用任何访问控制符修饰,就称它是包访问权限的,default访问控制的成员或外部类可以被相同包下的其他类访问

  • protected(子类访问权限):即可以被同一个包中的其他类访问,也可以被不同包中的子类访问。

  • public(公共访问权限):可以被所有类访问

private default protected public
同一个类中
同一个包中
子类中
全局范围内

封装和隐藏

封装(Encapsulation)是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问

对一个类或对象实现良好的封装,可以实现以下目的

  • 隐藏类的实现细节
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问
  • 可进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高代码的可维护性

实现良好的封装,需要从两个方面考虑

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
  • 把方法暴漏出来,让方法来控制对这些成员变量进行安全的访问操作

类的继承

继承是面向对象三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点。每个子类只能有一个父类。

继承的特点

Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类

Java 里子类继承父类的方式只需在原来的类定义上增加extends SuperClass(父类)即可

修饰符 class SubClass extends SuperClass{
    //类定义部分
}

子类可以从其父类中获得成员变量、方法和内部类(包括内部接口,枚举),不能获得构造器和初始化块

重写父类的方法

子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。

方法的重写要遵循“两同两小一大”规则:“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限比父类方法的访问权限更大或相等

覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法

super限定

super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法

如果子类定义了和父类相同的实例变量,则会发生子类实例变量隐藏父类实例变量的情形,正常情况下,子类定义的方法直接访问该实例变量会默认访问到子类中定义的实例变量,无法访问到父类中被隐藏的实力变量。在子类定义的实例方法中可以通过super来访问父类中被隐藏的实例变量

public class BaseClass {
    public int a = 5;
}
public class SubClass extends BaseClass {
    public int a = 7;
    public void accessOwner(){
        System.out.println(a);
    }
    public void accessBase(){
        System.out.println(super.a);
    }
}
public static void main(String[] args) {
    SubClass sc = new SubClass();
    sc.accessBase();
    sc.accessOwner();
}
5
7

调用父类构造器

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,在子类构造器中调用父类构造器使用super调用完成

不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况

  • 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器
  • 子类构造器执行体的第一行代码使用this显示调用本类父类构造器,系统将根据super调用里传入的实传入的实参列表调用本类中的另一个构造器
  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器

当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次向上溯执行其父类构造器...以此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器

public class Creature {
    public Creature(){
        System.out.println("Creature无参数的构造器");
    }
}
public class Animal extends Creature {
    public Animal(String name){
        System.out.println("Animal带一个参数的构造器," + "该动物的name为" + name);
    }
    public Animal(String name, int age){
        this(name);
        System.out.println("Animal代两个参数的构造器," + "其age为" + age);
    }
}
public class Wolf extends Animal {
    public Wolf(){
        super("灰太狼",12);
        System.out.println("Wolf 无参数的构造器");
    }
    public static void main(String[] args) {
        Wolf wolf = new Wolf();
    }
}
Creature无参数的构造器
Animal带一个参数的构造器,该动物的name为灰太狼
Animal代两个参数的构造器,其age为12
Wolf 无参数的构造器

多态

Java引用变量有两个类型:一个是编译类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型有实际赋给变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。

多态性

当把一个子类对象直接付给父类引用变量后,运行时调用该引用变量的方法时,其方法行为总是表现出子类的方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法是呈现出多种不同的行为特征,这就是多态

public class BaseClass {
    public void run(){
        System.out.println("执行BaseClass的run方法");
    }
}
public class SubClass extends BaseClass {
    public void run(){
        System.out.println("执行SubClass的run方法");
    }
}
public static void main(String[] args) {
    BaseClass sc = new SubClass();
    sc.run();
    System.out.println("运行时类型:" + sc.getClass());
}
执行SubClass的run方法
运行时类型:class SubClass

引用变量在编译阶段只能调用其编译类型时类型所具有的方法,但运行时则执行它运行时类型所具有的方法

通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量

引用类型的强制转换

引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时会出现错误。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行,否则将在运行时引发ClassCastException异常。

public static void main(String[] args) {
    // SubClass是BaseClass类型的子类 可转换
    SubClass subClass = new SubClass();
    BaseClass baseClass1 = (SubClass)subClass;
    // 编译时是BaseClass类型,运行时为SubClass类型,可转换
    BaseClass baseClass2 = new SubClass();
    SubClass subClass1 = (SubClass) baseClass2;
    // 父类实例强转子类类型 引发ClassCastException异常
    BaseClass baseClass3 = new BaseClass();
    SubClass subClass2 = (SubClass) baseClass3;
}

instanceof 运算符

instanceof运算符的钱一个操作数通常是一个引用类型变量,后一个操作数通常是一个类,它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false

public static void main(String[] args) {
    SubClass subClass1 = new SubClass();
    if(subClass1 instanceof BaseClass){
        BaseClass baseClass = (SubClass)subClass1;
    }
    BaseClass baseClass1 = new SubClass();
    if(baseClass1 instanceof SubClass){
        SubClass subClass = (SubClass) baseClass1;
    }
    // 这里增加通过instanceof判断是否可以转换,可以保证程序不会出现错误
    BaseClass baseClass2 = new BaseClass();
    if(baseClass2 instanceof SubClass){
        SubClass subClass = (SubClass) baseClass2;
    }
    // 没有继承关系所以引起编译错误,Inconvertible types; cannot cast 'SubClass' to 'Fruit'
    if(subClass1 instanceof Fruit){

    } 
}

isntanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误

初始化块

初始化块是Java类里可出现的第4种成员(前面依次有成员变量、方法和构造器),一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。

初始化块的语法格式如下

[修饰符] {
    // 初始化块的可执行性代码
    ...
}

初始化块的修饰符只能是static,使用static修饰的初始化块被称为类初始化块(静态初始化块),没有static修饰的初始化块被称为实例初始化块(非静态初始化块)。

实例初始化块

初始化块里的代码可以包含任何可执行的语句,包括定义局部变量、调用其他对象的方法,以及使用分支、循环语句等

public class Person {
    {
        int a = 4;
        if(a > 3){
            System.out.println("Person的实例初始化块:局部变量a大于3");
        }
        System.out.println("Person的实例初始化块");
    }
    {
        System.out.println("Person的第二个实例初始化块");
    }
    public Person(){
        System.out.println("Person的无参数构造器");
    }
    public static void main(String[] args) {
        Person person = new Person();
    }
}
Person的实例初始化块:局部变量a大于3
Person的实例初始化块
Person的第二个实例初始化块
Person的无参数构造器

实例初始化块只在创建Java对象时隐式执行,而且在构造器执行之前自动执行。类初始化则在类初始化阶段自动执行

实际上实例初始化块是一个假象,使用javac命令编译Java类后,该Java类中的实例初始化块会消失—实例初始化块种代码会被“还原”到每个构造器中,且位于构造器所有代码的前面

与构造器类似,创建一个Java对象时,不仅会执行该类的实例初始化器和构造器,而且系统会一直上溯到java.lang.Object类,先执行java.lang.Object类的实例初始化块,开始执行java.lang.Object的构造器,依次向下执行其父类的实例初始化块,开始执行其弗雷德构造器......最后才执行该类的实例初始化块和构造器

类初始化块

如果定义初始化块时使用了static修饰符,则这个初始化块就变成了类初始化块,也被称为静态初始化块。

类初始化块时类相关的,系统将在类初始化阶段执行类初始化块,而不是在创建对象是才执行。因此类初始化块总是比实例初始化块先执行

同样的,系统在类初始化阶段执行类初始化时,不仅会执行本类的类初始化,而且会一直上溯到java.object.Object类(如果它包含类初始化块)

public class Root {
    static {
        System.out.println("Root的类初始化块");
    }
    {
        System.out.println("Root的实例初始化块");
    }
}
public class Mid extends Root{
    static {
        System.out.println("Mid的类初始化块");
    }
    {
        System.out.println("Mid的实例初始化块");
    }

    public Mid(){
        System.out.println("Mid的无参数构造器");
    }
    public Mid(String name){
        this();
        System.out.println("Mid的带参数构造器,其参数值:" + name);
    }
}
public class Leaf extends Mid {
    static {
        System.out.println("Leaf的类初始化块");
    }
    {
        System.out.println("Leaf的实例初始化块");
    }

    public Leaf(){
        super("Leaf");
        System.out.println("Leaf的无参数构造器");
    }
}
public class Test {
    public static void main(String[] args) {
        new Leaf();
    }
}
// 类初始化阶段,先执行最顶层父类的类初始化块,然后依次向下,直到执行当前类的类初始化块
Root的类初始化块
Mid的类初始化块
Leaf的类初始化块
// 对象初始化阶段
Root的实例初始化块
Mid的实例初始化块
Mid的无参数构造器
Mid的带参数构造器,其参数值:Leaf
Leaf的实例初始化块
Leaf的无参数构造器
// 类初始化完毕后会一直在虚拟机里存在,无须再进行类初始化
Root的实例初始化块
Mid的实例初始化块
Mid的无参数构造器
Mid的带参数构造器,其参数值:Leaf
Leaf的实例初始化块
Leaf的无参数构造器

初始化块和声明变量时所指定的初始值都属于初始化代码,执行顺序会按照代码的先后顺序执行

public class Test {
    {
        a = 10;
    }
    int a = 9;
    public static void main(String[] args) {
        Test test = new Test();
        //输出9
        System.out.println(test.a);
    }
}

包装类

为了解决八种基本类型不能当成Object类型变量使用的问题,java提供了包装类(Wrapper Class)的概念,为八种基本类型定义了相应的引用类型,并称之为八种基本类型的包装类

基本数据类型 包 装 类
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean

JDK1.5提供了自动装箱和自动拆箱的功能,自动装箱:把一个基本类型变量直接赋值给对应的包装类变量或者赋值给Object变量,自动拆箱与之相反

Integer inObj = 5; //通过 自动装箱
Object obj = true; // 通过 自动装箱
int it = inObj // 通过 自动拆箱
if(obj instanceof boolean){
    //通过强制转换成Boolean对象,在自动拆箱为boolean
    boolean bl = (Boolean)obj;
}

包装类还可以实现基本类型变量和字符串之间的转换

  • 利用包装类提供parseXxx(String s)静态方法(除了Character都提供了该方法)
  • 利用包装类提供的valueOf(String s)静态方法。

处理对象

toString方法

toString方法时Object类里的一个实例方法,因此所有的Java对象都具有toString方法,Object类提供的toString()方法总是返回对象实现类的“类名+@+hashCode”值,我们可以通过重写toString方法来让对象输出该对象的内容

public class Apple {
    String Color;
    double weight;

    public Apple(String color, double weight) {
        Color = color;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "Color='" + Color + '\'' +
                ", weight=" + weight +
                '}';
    }
}
public static void main(String[] args) {
    Apple apple = new Apple("red",5.68);
    //println()方法自动输出对象的toString()方法的返回值
    System.out.println(apple);
}
Apple{Color='red', weight=5.68}

==和equals方法

Java程序中测试两个变量是否相等的方式有两个:一种时利用==运算符,另一种是利用equals()方法。

当使用==来判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型,只要两个变量的值相等,就将返回true。

但对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回true

当Java程序直接使用形如"hello"的字符串直接量(或者在编译时就计算出来的字符串值)是,JVM将会使用常量池来管理这些字符串;当使用new String("hello")时,JVM会先使用常量池来管理"hello"直接量,再调用String的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。

JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本

常量池专门用于管理在编译期间被确认并保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口中的常量,还包括字符串常量

public static void main(String[] args) {
    String s1 = "这是一个测试";
    String s2 = "这是";
    String s3 = "一个测试";
    String s4 = s2 + s3;
    String s5 = "这" + "是一个测试";
    String s6 = new String("这是一个测试");
	//s4无法在编译时确定下来值
    System.out.println(s1 == s4);
    //s5在编译时就已经确认下来
    System.out.println(s1 == s5);
    //s4是新创建String对象,存储在堆内存中
    System.out.println(s1 == s6);

    System.out.println(s1.equals(s4));
    System.out.println(s1.equals(s5));
    System.out.println(s1.equals(s6));
}
false
true
false
true
true
true

String 重写Object的equals()方法,判断两个字符串相等的标准是:只要两个字符串包含的字符序列相同,通过equals比较将返回true,否则返回false

equals()是Object类提供的一个实例方法,所有引用变量均可通过此方法来判断与其他引用变量是否相等。但使用Object提供的equals()方法判断两个对象相等和==没有区别。我们可以采用重写equals方法来实现自定义equals()方法

正确重写equals()方法应该满足一下条件

  • 自反性:对任意x,x.equals(x)一定返回true
  • 对称性:对任何x和y,如果y.equals(x)返回true,则x.equals(y)也返回true
  • 传递性:对任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true
  • 一致性:对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果都应该是一直
  • 对任何不是null的x,x.equals(null)一定返回false

静态成员

static关键字修饰的成员就是静态成员,static关键字不能修饰构造器。static修饰的成员属于整个类,不属于单个实例

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包含接口、枚举)五种成员,其中static可以修饰成员变量、方法、初始化块、内部类,以static修饰的成员就是静态成员,静态成员属于整个类,而不属于单个对象。

当通过类对象访问静态成员时,系统会在底层转换为通过该类来访问静态变量

public class NullAccessStatic {
    public static void test(){
        System.out.println("静态方法执行了");
    }
}
public static void main(String[] args) {
    NullAccessStatic nullAccessStatic = null;
    nullAccessStatic.test();
}

执行结果

静态方法执行了

final 修饰符

final关键字可用于修饰类、变量和方法,表示它修饰的类、变量和方法不可变。final修饰变量时,表示该变量一旦获得了初始值就不可被改变

final 成员变量

final修饰的成员变量必须由程序员显示地指定初始值

final修饰的成员变量,静态变量能指定初始值的地方如下

  • 静态变量:必须在静态初始块中指定初始值或声明该变量时指定初始值,而且只能在两个地方中的其中之一指定
  • 实例变量:必须在非静态初始化块、声明该变量时或构造器中指定初始值,只能在三个地方中的其中之一指定
public class Test {
    final int a = 0;
    final String str;
    final int c;
    static final int d;

    {
        // 在初始化块中指定初始值
        str = "";
        // 定义a变量时已经指定了默认值,非法
        // a = 2;
    }

    static {
        // 在静态初始化块中指定初始值
        d = 2;
    }
    public Test(){
        // 在构造器中指定初始值
        c = 5;
        // 初始化块中已被指定初始值,下面代码报错
        // str = "123";
    }
}

如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量;如果在初始化之前访问会直接报错

public class Test {
    final int age;
    {
        // 没有初始化无法直接访问所以报错
        // System.out.println(age);
        // 可以通过方法类访问
        printAge(); // 输出0
        age = 6;
        System.out.println(age);
    }

    public void printAge(){
        System.out.println(age);
    }
    public static void main(String[] args) {
        new Test();
    }
}

final成员变量在显示初始化之前不能直接访问,但可以通过方法来访问,这是java设计的一个缺陷。按照正常逻辑,final成员变量在显式初始化之前时不应该允许被访问的。

final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此final修饰局部变量时,既可以在定义是指定默认值,也可以不指定默认值。但只能赋值一次,不能重复赋值。

public static void main(String[] args) {
    final String a = "hello";
    final int b;

    // 变量a已被赋值,所以下面语句非法
    // a = "321";
    // 赋初始值
    b = 3;
    // 重复赋值报错
    // b = 4;

}

public void test(final int a){
    // 重复赋值报错
    // a = 12;
}

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型不能被改变,但对于引用类型来说,它保存的仅仅是一个引用,final只能保证这个引用类型所引用的地址不能被改变,引用的这个对象完全可以发生改变

可执行“宏替换”的final变量

对于一个final变量来说,不管它是静态变量、实例变量,还是局部变量,只要改变量满足三个条件,这个final变量就不再是一个变量,而是 相当于一个直接量

  1. 使用final修饰符修饰
  2. 在定义该final变量时指定了初始值
  3. 该初始值可以在编译时就被确定下来
//普通变量
String s1 = "hello";
String s2 = " world";
//由于s1,s2是两个变量,无法在编译时确定s的值
String s = s1 + s2;

//直接量
final String sf1 = "hello";
final String sf2 = " world";

//由于sf1,sf2是两个直接量,所以可以在编译时确定sf的值,即指向字符串池中缓存的字符串
String sf = sf1 + sf2;

//由于s没有指向字符串池中的字符串,所以结果为false
System.out.println("hello world" == s);
//sf在编译时就已经确定,sf的值是指向字符串池中的字符串,所以结果为true
System.out.println("hello world" == sf);

Java会使用常量池来管理曾经用的字符串直接量,例如执行String a = "java";语句后,常量池中就会缓存一个字符串“java”;如果程序在执行String b= "java";,系统将会让b直接指向常量池中的“java”字符串,因此a==b将会返回true

final方法

final修饰的方法不可被重写

public class Test {

    //Object类里有个final方法getClass(); 所以下行代码会报错
    //'getClass()' cannot override 'getClass()' in 'java.lang.Object'; overridden method is final
    public void getClass(){}
}

对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法。如果子类定义了一个与父类private方法有相同方法名、相同参数列表、相同返回值的方法,也不是方法重写,只是重新定义了一个方法。因此,即使使用final修饰一个private访问权限的方法,依旧可以在子类定义于该方法具有相同方法名、相同形参列表、相同返回值类型的方法。

final类

final修饰的类不可以有子类

public final class FinalClass {}
//下面的将出现编译错误
class sub extends FinalClass {}

抽象类

抽象方法和抽象类

抽象方法和抽象类必须使用abstract修饰符类定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法

抽象方法和抽象类的规则如下

  • 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体
  • 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例
  • 抽象类可以包含成员变量、方法、构造器、初始化块、内部类
  • 含有抽象方法的类只能被定义成抽象类

定义抽象方法需要在普通方法上增加abstract修饰符,并把普通方法的方法体(也就是方法后花括号括起来的的部分)全部去掉,并在方法后增加分号即可。

定义抽象类只需在普通类上增加abstract修饰符即可。

public abstract class Shape {
    {
        System.out.println("执行Shape的初始化块");
    }
    
    private String color;
    // 定义一个计算周长的方法
    public abstract double calPerimeter();
    // 定义一个返回形状的放啊发
    public abstract String getType();
    public Shape(){
        
    }
    public Shape(String color){
        System.out.println("执行Shape的构造器");
        this.color = color;
    }
    
}
public class Triangle extends Shape{

    //定义三角形的三边
    private double a;
    private double b;
    private double c;

    public Triangle(String color,double a,double b,double c){
        super(color);
        this.setSides(a,b,c);
    }

    public void setSides(double a,double b,double c){
        if(a >= b + c || b >= a + c || c >= a + b){
            System.out.println("三角形两边之和必须大于第三边");
            return;
        }
        this.a = a;
        this.b = b;
        this.c = c;
    }

    // 重写父类的周长方法
    @Override
    public double calPerimeter() {
        return a + b + c;
    }

    // 重写父类返回形状的方法
    @Override
    public String getType() {
        return "三角形";
    }
}

利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活

当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用

接口

接口用于定义某一批类所需要遵循的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,只规定这批类里必须提供某些方法。

接口的定义

和类定义不同,定义接口不再使用class关键字,而是使用interface关键字。接口定义的基本语法如下

[修饰符] interface 接口名 extends 父接口1,父接口2...
{
    零到多个常量定义...
    零到多个抽象方法定义...
    零到多个内部类、接口、枚举定义...
    零到多个私有方法、默认方法或静态方法定义...
}
  • 修饰符可以是public或者省略,如果省略了public访问控制符,默认采用protect访问控制符
  • 接口名应与类名采用相同的命名规则
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类

在接口中定义成员变量时,不管是否适用public、static、final修饰符,接口里的成员变量总是用着三个修饰符来修饰。而且接口里面没有构造器和初始化块,因此接口里定义成员变量只能在定义时指定默认值。

接口里的定义的方法只能是抽象方法、静态方法、默认方法或私有方法,因此如果不是定义默认方法、类方法或私有方法,系统将自动为普通方法增加abstract修饰符

从Java8开始,在即口里允许定义默认方法,默认方法必须使用default修饰,该方法不能使用static修饰,无论程序是否指定,默认方法总是使用public修饰

public interface IOutPut {
    // 接口里定义的成员变量总是用public static final来修饰,所以以下两行代码一样
    //public final static int MAX_CACHE_LINE = 50;
    int MAX_CACHE_LINE = 50;
    
    // 在接口中定义默认方法,需要用default修饰
    default void print(String msg){
        System.out.println(msg);
    }
    // 在接口中定义静态方法
    static void staticTest(){
        System.out.println("接口里的静态方法");
    }
    //普通方法自动增加abstract修饰符
    public void test();
}

接口的继承

接口支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量

一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间可以以英文逗号(,)隔开

public interface InterfaceA {
    int PROP_A = 5;
    void testA();
}
public interface InterfaceB {
    int PROP_B = 5;
    void testB();
}
public interface InterfaceC extends InterfaceA,InterfaceB{
    int PROP_C = 7;
    void testC();
}
public static void main(String[] args) {
    System.out.println(InterfaceC.PROP_A);
    System.out.println(InterfaceC.PROP_B);
    System.out.println(InterfaceC.PROP_C);
}

接口的实现

一个类可以实现一个或多个接口,通过implements关键字实现接口

类实现接口的语法格式如下

[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
	类体部分
}

实现接口与继承父类相似,一样可以获得所实现接口里定义的常量(成员变量)、方法(包括抽象方法和默认方法),implements部分必须放在extends部分之后。

一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口哪里继承得到的抽象方法,该类也必须定义成抽象类。

public class InterfaceImpl implements InterfaceC{
    @Override
    public void testA() {
        System.out.println(PROP_A);
    }

    @Override
    public void testB() {
        System.out.println(PROP_B);
    }

    @Override
    public void testC() {
        System.out.println(PROP_C);
    }
}

实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法是访问权限只能更大或相等,所以实现类实现接口里的方法时只能使用public访问权限

接口和抽象类

接口和抽象类很像,它们都具有以下特征

  • 接口和抽象类都不能被实例化。
  • 接口和抽象类都可以包含抽象方法。 实现接口或继承抽象类的普通子类都必须实现这些抽象方法

除此之外,接口和抽象类在用法上也存在如下差别

  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法(java9),不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  • 接口里不能包含初始化块;但抽像类则可以完全包含初始化块。
  • 一个类最多只能有一个直接父类,包括轴向类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足

内部类

某项情况下,会把一个类放在另一个类的内部定义,这个定义在其它类内部的类就被称为内部类(嵌套类),包含内部类的类也被称为外部类(宿主类)

内部类主要有一下作用

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包的其他类访问该类
  • 内部类成员可以直接访问外部类的私有数据
  • 匿名内部类适合用于创建那些仅需要一次使用的类。

内部类除了需要定义在其他类里面之外,存在以下两条区别

  • 内部类比外部类可以多使用三个修饰符:private、protected、static—外部类不可以使用这三个修饰符
  • 非静态内部类不能拥有静态成员

非静态内部类

内部类定义语法格式如下

public class OuterClass
{
    //此处定义内部类
}

因为内部类作为其外部类的成员,所以可以使用任意访问控制符如private,protectedpublic等修饰

非静态内部类可以直接访问外部类的任何成员

public class Cow {
    private double weight;
    public Cow(){}
    public Cow(double weight){
        this.weight = weight;
        double temp = new CowLeg().length;
    }

    //定义一个内部类
    private class CowLeg{
        private double length;
        private String color;

        public CowLeg(){}
        public CowLeg(double length,String color){
            this.length = length;
            this.color = color;
        }

        public void info(){
            System.out.println("当前牛腿颜色是"+color+",高:"+length);
            //直接访问外部类的private修饰的的成员变量
            System.out.println("本牛腿所在奶牛重" + weight);
        }
    }
    public static void main(){
        CowLeg cowLeg = new Cow(). new CowLeg();
    }
}

如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,可以通过使用this、外部类类名.this 作为限定来区分

System.out.println("本牛腿所在奶牛重" + Cow.this.weight);

在非静态内部对象里,保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须集成在外部类实例里)

非静态内部类不能有静态方法、静态成员变量、静态初始化块

静态内部类

如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象,因此使用static修饰的内部类称为静态内部类

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。即使时静态内部类的实例方法也不能访问外部类的实例成员,之恩那个访问外部类的静态成员

public class StaticInnerClassTest {
    private int prop1 = 5;
    private static int prop2 = 9;

    static class StaticInnerClass {
        // 静态内部类可以包含静态成员
        private static int age;
        public void accessOuterProp(){
            //静态内部类无法访问外部类的实例变量,报错
            System.out.println(prop1);
            System.out.println(prop2);
        }
    }
}

外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类中的静态成员,也可以使用静态内部类对象作为调用者来访问讲台内部类的实例成员

public class AccessStaticInnerClass {
    static class StaticInnerClass {
        private static int prop1 = 5;
        private int prop2 = 9;
    }

    public void accessInnerProp() {
        //通过类名访问静态内部类的静态成员
        System.out.println(StaticInnerClass.prop1);
        //通过实例访问静态内部类的实例成员
        System.out.println(new StaticInnerClass().prop2);
    }
}

除此之外,Java允许在接口定义内部类,接口里定义的内部类默认用public static修饰,也就是说,接口内部类只能是静态内部类

如果为接口内部类指定访问控制符,则只能是public访问控制符;如果接口定义接口内部类是省略访问控制符,则该内部类默认是public访问控制权限

使用内部类

在外部类内部使用内部类

在外部类内部使用内部类时,与平常使用普通类不没太大的区别。一样可以直接通过内部类类名定义变量,通过new调用内部类构造器来创建实例

唯一的区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员

在外部类以外使用非静态类

如果希望在外部类以外的地方访问内部类(包括静态和非静态),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用

对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用

  • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问
  • 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问
  • 使用public修饰的内部类,可以在任何地方被访问.

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下

OuterClass.InnerClass varName

在外部类以外的地方创建非静态内部类实例的语法如下

outerInstance.new InnerConstructor()
public class Out {
    class In{
        public In(String msg){
            System.out.println(msg);
        }
    }
}
public static void main(String[] args) {
    Out.In in = new Out().new In("helloword");
}

在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象

public class SubClass extends Out.In {
    //显示定义SubClass的构造器
    public SubClass(Out out) {
        // 通过传入的Out对象显示调用In的构造器
        out.super("hello");
    }
}

在外部类意外使用静态内部类

在外部类以外的地方创建静态内部类实例的语法如下

new OuterClass.InnerConstructor()

事例

public class StaticOut {
    static class StaticIn{
        public StaticIn(){
            System.out.println("静态内部类的构造器");
        }
    }
}
public static void main(String[] args){
    StaticOut.StaticIn in = new StaticOut.StaticIn();
}

局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能再外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰

如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行

public class LocalInnerClass {
    public static void main(String[] args){
        // 定义局部内部类
        class InnerClass{
            int a;
        }
        // 定义局部内部类的子类
        class InnerSub extends InnerClass{
            int b;
        }
        // 创建局部内部类对象
        InnerSub is = new InnerSub();
        is.a = 5;
        is.b = 9;
    }
}

匿名内部类

匿名内部类适合创建那种只需要一次使用的类,创建匿名内部类是会立即创建一个该类的实力,这个类定义立即小时,匿名内部类不能重复使用

定义匿名内部类的格式如下

new 实现接口() | 父类构造器(实在列表)
{
    //匿名内部类的类体部分
}

匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个类或实现一个接口

关于匿名内部类还有如下两条规则

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类
  • 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情
public interface Product {
    double getPrice();
    String getName();
}
public class AnoymousTest {
    public void test(Product p){
        System.out.println("购买了一个" + p.getName() + ",花掉了" + p.getPrice());
    }

    public static void main(String[] args){
        AnoymousTest an = new AnoymousTest();
        // 传入其匿名实现类的实例
        an.test(new Product() {
            @Override
            public double getPrice() {
                return 13000;
            }

            @Override
            public String getName() {
                return "4090显卡";
            }
        });
    }
}

通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表

public abstract class Device {
    private String name;
    public abstract double getPrice();
    public Device(){};
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Device(String name){
        this.name = name;
    }
}
package 匿名内部类;

public class AnoymousInner {
    public void test(Device d) {
        System.out.println("购买了一个" + d.getName() + ",花掉了" + d.getPrice());
    }

    public static void main(String[] args) {
        AnoymousInner ai = new AnoymousInner();
        //调用有参数的构造器创建Device匿名实现类的对象
        ai.test(new Device("4090") {
            @Override
            public double getPrice() {
                return 13000;
            }
        });
        //调用无参数的构造器创建Device匿名实现类的对象
        ai.test(new Device() {
            //初始化块
            {
                System.out.println("初始化匿名内部类的初始化块");
            }

            // 实现抽象方法
            @Override
            public double getPrice() {
                return 56.2;
            }

            // 重写父类的实例方法
            @Override
            public String getName() {
                return "键盘";
            }
        });
    }
}

购买了一个4090,花掉了13000.0
初始化匿名内部类的初始化块
购买了一个键盘,花掉了56.2

在Java 8 之前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰,从Java 8 开始这个限制被取消了,Java 8 更加智能:如果局部变量被内部类访问,那么该局部变量相当于自动使用final修饰

public static void main(String[] args) {
    AnoymousInner ai = new AnoymousInner();
    int a = 12;
    ai.test(new Device("4090") {
        @Override
        public double getPrice() {
            // 报错 Variable 'a' is accessed from within inner class, needs to be final or effectively final
            a = 2;
            return 13000;
        }
    });
}

Lambda 表达式

Lambda基础

Lambda 表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法,静态方法,但只能声明一个抽象方法。

如果采用匿名内部类语法来创建函数式接口的实例,则只需要要实现一个抽象方法,在这种情况下可采用Lambda表达式来创建对象。Java 8 含有大量函数式接口,例如 Runnable、ActionListener等接口都是函数时接口

Java 8 专门为函数式接口提供了@FunctionalInterface注解,该注解通常被放在解耦定义的前面,该注解对程序过程没有任何作用,它用于告诉编译器执行更严格检查(检查该接口必须是函数式接口,否则编译器会报错)

public static void main(String[] args) {
    //Runnable 是Java本身提供的一个函数式接口
    Runnable runnable = () -> {
        for (int i = 0; i < 20; i++) {
            System.out.println(i);
        }
    };
    // 如果Lambda表达式只有一条代码,程序可以省略Lambda表达式中的花括号
    Runnable runnable2 = () -> System.out.println("123");
}

Lambda表达式有如下两个限制

  • Lambda 表达式的目标类型必须是明确的函数时接口
  • Lambda 表达式只能为函数式接口创建对象。Lambda 表达式只能实现一个方法,因此它只能为只有一个抽象想方法的接口(函数式接口)创建对象。

方法引用和构造器引用

方法引用和构造器引用都可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号

种 类 示 例 说 明 对应的的Lambda表达式
引用静态方法 类名::静态方法 函数式接口中被实现方法的全部参数传给该类方法作为参数 (a,b,...)->类名.静态方法(a,b,...)
引用特定对象的实例方法 特定对象::实例方法 函数式接口中被实现方法的全部参数传给该方法作为参数 (a,b,...)->实例.实例方法(a,b,...)
引用某类对象的实例方法 类名::实例方法 函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数 (a,b,...)->a.实例方法(b,...)
引用构造器 类名::new 函数式接口中被实现方法的全部参数传给该构造器作为引用参数 (a,b,...)->new 类名(a,b,...)

引用静态方法

@FunctionalInterface
public interface Converter {
    Integer convert(String from);
}
 Converter converter = from -> Integer.valueOf(from);

上面Lambda表达式的代码块只有一条语句,因此程序省略了改代码块的花括号;而且由于表达式所实现的convert()方法需要返回值,因此Lambda表达式将会把这条代码的值作为返回值

下面使用静态方法引用进行替换

// 函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter converter = Integer::valueOf;

当上述代码调用Converter接口中唯一的抽象方法时,调用参数将会传给Integer类的valueOf()静态方法

引用特定对象的实例方法

Converter converter = from -> "hello".indexOf(from);

使用实例方法转换

Converter converter = "hello"::indexOf;

当调用Converter接口中唯一的抽象方法时,调用参数将会传给"hello"对象的IndexOf()实例方法

引用某类对象的实例方法

@FunctionalInterface
public interface MyTest {
    String test(String a, int b, int c);
}

使用Lambda表达式创建一个对象

MyTest mt = (a, b, c) -> a.substring(b, c);
String str = mt.test("Java I Love", 2, 9);
System.out.println(str); //输出 va I Lo

替换方式

MyTest mt = String::substring;

当调用Converter接口中唯一的抽象方法时,第一个调用参数将作为substring()方法的调用者,剩下的调用参数会作为substring()实例方法的调用参数

引用构造器

函数式接口中抽象方法的返回值的类中的构造函数的形参和函数式接口中抽象方法的形参一致时,可以同过引用构造器的方式来创建实例对象

public class Person {
    private String name;
    public Person(String name){
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
@FunctionalInterface
public interface ConstructionTest {
    public Person getPerson(String name);
}

Lambda表达式

public static void main(String[] args) {
    ConstructionTest ct = name -> new Person(name);
    Person person = ct.getPerson("张三");
}

引用构造器

public static void main(String[] args) {
    ConstructionTest ct = Person::new;
    Person person = ct.getPerson("张三");
}

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

Lambda 表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda 表达式与匿名内部类存在如下相同点

  • Lambda 表达式与匿名内部类一样,都可以直接访问 “effectively final” 的局部变量,以及外部类的成员变量(包括实例变量和静态变量)
  • Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法
@FunctionalInterface
public interface Displayable {
    void display();
    default int add(int a,int b){
        return a + b;
    }
}
public class LambdaAndInner {
    private int age = 12;
    private static String name = "张三";

    public void test() {
        String book = "hello world!";
        Displayable dis = () -> {
            // 访问外部类的实例变量
            System.out.println(age);
            // 访问外部类的静态变量
            System.out.println(name);
            // 调用局部变量
            System.out.println(book);
        };
        dis.display();
        // 调用默认方法
        dis.add(1, 2);
    }
}

Lambda 表达式与匿名内部类主要存在以下区别

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象想方法,只要匿名内部类实现所有的抽象方法即可;但Lambda 表达式只能为函数式接口创建实例
  • 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法; 但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。

枚举类

枚举类入门

Java 5 新增了一个enum关键字(它与class、interface关键字的地位相同),用于定义枚举类。枚举类一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。

一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同

枚举类和普通类的简单区别

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显示继承其他父类。其中 java.lang.Enum类实现了java.lang.Serializablejava.lang.Comparable两个接口
  • 使用enum定义、非抽象的枚举类默认会使用final修饰。
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰。由于枚举类的所有构造器都是private的,因此枚举类也不能派生子类
  • 枚举类的所有实例必须在枚举类的第一行显示列出,否则这个枚举类永远都不能产生实例。列出这些事例时,系统会自动添加public static final修饰,无须程序员显式添加

枚举类默认提供了一个values()方法,该方法可以很方便地便利所有的枚举值

定义枚举类时,需要显示列出所有的枚举值,所有的枚举值之间以英文逗号(,)隔开,枚举值列举结束后以英文分号作为结束。这些枚举值代表了该枚举类的所有可能的实例

public enum SeasonEnum {
    SPRING, SUMMER, FALL, WINTER;
}
public class EnumTest {
    public static void judge(SeasonEnum s){
        switch (s){
            case SPRING:
                System.out.println("春暖花开");
                break;
            case SUMMER:
                System.out.println("夏日炎炎");
                break;
            case FALL:
                System.out.println("秋高气爽");
                break;
            case WINTER:
                System.out.println("冬日雪飘");
                break;
        }
    }

    public static void main(String[] args) {
        // 枚举类默认有一个values()方法,返回该枚举类的所有实例
        for (SeasonEnum s : SeasonEnum.values()){
            judge(s);
        }
        // 使用枚举实例时,可通过EnumClass.variable 形式来访问
        judge(SeasonEnum.SUMMER);
    }
}

switch的控制表达式可以是任何枚举类型。当switch控制表达式使用枚举类型时,后面case表达式中的值直接使用枚举值的名字,无需添加枚举类作为限定

所有枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中包含的方法。java.lang.Enum类中提供了如下几个方法

  • int compareTo(E o) 该方法用于指定枚举对象比较顺序,同一个枚举实例只能与相同类型枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回增证书;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零
  • String name() 返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一
  • int ordinal() 返回枚举值在枚举类的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引为0)
  • String toString() 返回枚举常量的名称,与name方法相似,但toString()方法更常用
  • public static<Textends Enum<T>>T valueOf(Class<T> enumType,String name) 这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符

枚举类的成员变量、方法和构造器

枚举类同样可以定义成员变量、方法和构造器,使用成员变量和方法与普通类没什么区别,差别只是产生对象实例的方式不同,枚举类的实例只能是枚举值,而不是随意通过new来创建枚举类对象

一旦为枚举类显示定义了带参数的构造器,列出枚举值时就必须对应地传入参数

public enum Gender {
    // 此处枚举值必须调用对应的构造器来创建
    MALE("男"),FEMALE("女");
    private Gender(String name){
        this.name = name;
    }
    private String name;
    public String memo;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public static void main(String[] args) {
    Gender.FEMALE.setName("男");
    Gender.valueOf("FEMALE").setName("女");
    Gender.FEMALE.memo = "备注";

    System.out.println("FEMALE中文释义:"+Gender.FEMALE.getName());
}

实现接口的枚举类

枚举类可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的抽象方法

public interface GenderDesc {
    void info();
}
public enum Gender implements GenderDesc{
    ...
    @Override
    public void info() {
        
    }
}

如果需要每个枚举值在调用该方法时呈现不同的行为方式,则可以让每个枚举值分别来实现该方法,每个枚举值提供不同的实现方式,从而让不同的枚举值调用该方法时具有不同的行为方式

public enum Gender implements GenderDesc {
    MALE("男") {
        @Override
        public void info() {
            System.out.println("这个枚举值代表男性");
        }
    }, FEMALE("女") {
        @Override
        public void info() {
            System.out.println("这个枚举值代表女性");
        }
    };

    private Gender(String name) {
        this.name = name;
    }
}

上面代码创建MALE和FEMALE枚举值时,并不是直接创建Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例

包含抽象方法的枚举类

枚举类里定义抽象方法时不能使用abstract关键字将枚举定义成抽象类(因为系统会自动为它添加abstract关键字),但因为枚举类需要显示创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误

public enum Operation {
    PLUS {
        @Override
        public double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        public double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        @Override
        public double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        public double eval(double x, double y) {
            return x / y;
        }
    };

    // 为枚举值定义一个抽象方法
    // 这个抽象方法有不同的枚举值提供不同的实现
    public abstract double eval(double x, double y);

    public static void main(String[] args) {
        System.out.println(Operation.PLUS.eval(2, 5));
        System.out.println(Operation.MINUS.eval(10, 2));
        System.out.println(Operation.TIMES.eval(2, 5));
        System.out.println(Operation.DIVIDE.eval(4, 2));
    }
}
7.0
8.0
10.0
2.0

对象与垃圾回收

当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为止分配一块内存区,对象就保存在这块内存中,当这块内存不再被任何引用变量引用时,这块内存就变成垃圾,等待垃圾回收机制进行回收。

垃圾回收机制具有如下特征

  • 垃圾回收机制只负责回收堆内存中的对象,不会回收任何物理资源(例如数据库连接,网络IO等资源)
  • 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性失去引用后,系统就会在合适的时候回收它所占的内存。
  • 在垃圾回收机制回收任何对象之前,总会调用它的finalize()方法,该方法可能使该对象重新复核(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收

对象在内存中的状态

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把他所处的状态分成一下三种。

  • 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法
  • 可恢复状态:对象不再有任何引用变量引用它,它就进入了可恢复状态,在这种状态下,系统在垃圾回收之前会调用此对象的finalize()方法进行资源清理。如果调用此方法时重新让一个变量引用该对象,那么该对象再次变为可达状态;否则进入不可达状态
  • 不可达状态:当对象永久性的失去引用后,即为不可达状态,系统才会真正回收该对象所占有的资源

public class StatusTranfer {
    public static void Test(){
        String a = new String("hello"); // "hello"被创建,并含有一个引用,进入可达状态
        a = new String("bye bye"); //"bye bye"被创建,并含有一个引用,进入可达状态。"hello"失去引用,进入可恢复状态,执行完finalize进入不可达状态
    }

    public static void main(String[] args) {
        Test();
        //Test()执行结束 "bye bye"失去引用,进入可恢复状态,执行完finalize进入不可达状态
    }
}

强制垃圾回收

程序无法精确控制Java 垃圾回收的时机,但依然可以强制系统进行垃圾回收——这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不稳定。

强制系统垃圾回收有如下两种方式

  • 调用System类的gc()静态方法:System.gc()
  • 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
public class GcTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new GcTest();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("系统正在清理GcTest对象的资源");
    }
}

运行以上程序没有任何输出

在程序上增加强制垃圾回收

public class GcTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new GcTest();
            // 强制系统进行垃圾回收
            Runtime.getRuntime().gc();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("系统正在清理GcTest对象的资源");
    }
}

输出

系统正在清理GcTest对象的资源
系统正在清理GcTest对象的资源
系统正在清理GcTest对象的资源
系统正在清理GcTest对象的资源
系统正在清理GcTest对象的资源
系统正在清理GcTest对象的资源

finalize 方法

在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定清理资源的情况下,Java提供了默认机制来清理该对象的资源,这个机制就是finalize()方法。 该方法时定义在Object类里的实例方法,方法原型为:

protected void finalize() throws Throwable

finalize()方法具有如下4个特点

  • 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用
  • finalize() 方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会执行的方法
  • 当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态
  • 当JVM执行finalize()方法出现异常时,垃圾回收机制不会报告异常,程序继续执行

对象的软、弱和虚引用

对大部分对象而言,程序里会有一个引用变量引用该对象,这是最常见的引用方式。除此之外,java.lang.ref包下提供了三个类:SoftReference PhantomReference WeakReference,它们分别代表了系统对对象的三种引用方式:软引用、虚引用和弱引用

Java语言对对象的引用有如下四种方式

  1. 强引用(StongReference)

这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或多个引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。

  1. 软引用(SoftReference)

软引用需要通过 SoftReference 类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中

  1. 弱引用(WeakReference)

弱引用通过 WeakReference类实现,弱引用和软引用很想,但弱引用的级别更低。对于只有弱引用的对象而言,当系统垃圾会机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。

  1. 虚引用(PhantomReference)

虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收时的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。

上面三个引用都包含了一个get()方法,用于获取被它们所引用的对象

引用队列由java.lang.ref.ReferenceQueue类表示,它用于保存被回收后对象的引用。当联合使用软引用、弱引用和引用对象列时,系统再回首被引用的对象之后,将把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它关联的引用队列中,这使得可以在对象被回收之前采取行动

弱引用示例

public class ReferenceTest {
    public static void main(String[] args) {
        // 创建一个字符串对象
        String str = new String("Java");
        // 创建一个弱引用,将此弱引用引用到 “Java”
        WeakReference wr = new WeakReference(str);
        // 切断str与 “Java”的引用
        str = null;
        // 取出弱引用所引用的对象
        System.out.println(wr.get());
        // 强制垃圾回收
        System.gc();
        System.runFinalization();
        System.out.println(wr.get());
    }
}

输出

Java
null

虚引用示例

public class PhantomReferenceTest {
    public static void main(String[] args) {
        String str= new String("Java");
        // 创建一个引用队列
        ReferenceQueue rq = new ReferenceQueue();
        // 创建一个虚引用,让此虚引用引用到“Java”
        PhantomReference pr = new PhantomReference(str,rq);
        // 切断str变量的引用
        str = null;
        // 取出虚引用所引用的对象,并不能通过虚引用获取被引用的对象,所以输出null
        System.out.println(pr.get());
        // 强制回收
        System.gc();
        System.runFinalization();
        // 垃圾回收之后,虚引用被放入引用队列之中
        // 取出引用队列中最先进入队列的引用与pr进行比较
        System.out.println(rq.poll() == pr); // true
    }
}

修饰符的适用范围

外部类接口 成员属性 方法 构造器 初始化块 成员内部类 局部成员
public
protected
包访问控制符
private
abstract
final
static
strictfp
synchroized
native
transient
volatile
default
  • strictfp 精确浮点。使用此关键字修饰类、接口或者方法时,所在范围会按照浮点规范IEEE-745来执行
  • native类似抽象方法。与抽象方法不同的是,native方法通常采用C语言来实现。如果某个方法需要利用平台相关特性,或者访问系统硬件等,则可以使用native修饰该方法,再把该方法交给C去实现。一旦Java程序中包含了native方法,这个程序将失去跨平台的功能

热门相关:流鱼无恙   重生野性时代   变身蜘蛛侠   重生野性时代   回眸医笑,冷王的神秘嫡妃