【Java SE】十、类

面向对象编程 (OOP) 的核心,Java 作为一门纯 OOP 语言,它的类也与 Python 有些不同,故在此记录不同之处

基本操作

声明

[修饰符] class 类名 {...}

继承

[修饰符] class 类名 extends 父类 {...} // 声明子类,只能单继承,父类后面可继续声明实现的接口

实现

[修饰符] class 类名 implements 接口1, 接口2... {...} // 声明实现接口的类,可以“多继承”

实例化

类名 实例名 = new 构造方法(); // 构造方法与类同名,若采用有参构造方法,则需传入参数

构造方法

也叫构造器,与 Python 类中的 __init__ 功能类似,是一个对象被创建时,用来初始化该对象的方法。构造方法和它所在类的名字相同,但构造方法没有返回值。

下面是一个使用构造方法的例子:

// 一个简单的构造函数,默认访问修饰符为default
class MyClass {
    int x;
 
    // 以下是构造函数,访问修饰符与类同为default,且没有声明返回值类型
    MyClass(int i) {
        x = i; // 构造方法常用来初始化变量,这里是直接访问类属性
    }
}

在没有显式定义构造方法的情况下,Java 自动提供了一个默认的无参构造方法,默认构造方法的访问修饰符和类的访问修饰符相同

你可以像下面这样调用构造方法来初始化一个对象:

public class ConsDemo {
    public static void main(String[] args) {
        MyClass t1 = new MyClass(10);
        MyClass t2 = new MyClass(20);
        System.out.println(t1.x + " " + t2.x); // 输出结果为:10 20
    }
}

注:一旦你定义了自己的构造方法,默认构造方法就会失效。


this 关键字

上述代码定义的方法可以直接访问变量,但如果方法里有个局部变量和静态变量同名,但程序又需要在该方法里访问这个被覆盖的静态变量,则必须使用 this 前缀,这个与 Python 中的 self 关键字类似,可以理解为指向对象本身的一个指针

假设有一个教师类 Teacher 的定义如下:

public class Teacher {
    private String name;      // 教师名称
    private double salary;    // 工资
    private int age;          // 年龄
}

在上述代码中 namesalary age 的作用域是 private,因此在类外部无法对它们的值进行设置。为了解决这个问题,可以为 Teacher 类添加一个构造方法,然后在构造方法中传递参数进行修改。代码如下:

// 创建构造方法,为上面的3个属性赋初始值
public Teacher(String name,double salary,int age) {
    this.name = name;      // 设置教师名称
    this.salary = salary;  // 设置教师工资
    this.age = age;        // 设置教师年龄
}

Teacher 类的构造方法中使用了 this 关键字对属性 namesalaryage 赋值,this 表示当前对象。this.name = name 语句表示一个赋值语句,等号左边的 this.name 是指当前对象具有的变量 name,等号右边的 name 表示参数传递过来的数值。


super 关键字

与 Python 类似,super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。super 的用法与 this 类似,只不过是在子类中的变量或方法与父类中的同名时,用来访问父类的变量或方法。如下:

class Country {
    String name;
    void value() {
       name = "China";
    }
}
  
class City extends Country {
    String name;
    void value() {
        name = "Shanghai";
        super.value(); // 访问父类的方法
        System.out.println(name);
        System.out.println(super.name); // 访问父类的属性
    }
  
    public static void main(String[] args) {
       City c=new City();
       c.value();
    }
}

注意:访问父类的属性和方法实际上是访问从父类继承来的属性和方法;也就是说,被访问的这些属性和方法是在子类上而不是父类上

通过关键字引用构造函数

  • super(参数):调用父类中的某一个构造函数。
  • this(参数):调用本类中另一种形式的构造函数。
  • 以上两条语句应该放在构造函数的第一条语句的位置,正因如此,两条语句不能同时出现。
class Person { 
    public static void prt(String s) { 
       System.out.println(s); 
    } 
   
    Person() { 
       prt("父类·无参数构造方法: "+"A Person."); 
    }//构造方法(1) 
    
    Person(String name) { 
       prt("父类·含一个参数的构造方法: "+"A person's name is " + name); 
    }//构造方法(2) 
} 
    
public class Chinese extends Person { 
    Chinese() { 
       super(); // 调用父类构造方法(1) 
       prt("子类·调用父类无参数构造方法": "+"A chinese coder."); 
    } 
    
    Chinese(String name) { 
       super(name);// 调用父类具有相同形参的构造方法(2) 
       prt("子类·调用父类含一个参数的构造方法": "+"his name is " + name); 
    } 
    
    Chinese(String name, int age) { 
       this(name);// 调用具有相同形参的构造方法(3) 
       prt("子类:调用子类具有相同形参的构造方法:his age is " + age); 
    } 
    
    public static void main(String[] args) { 
       Chinese cn = new Chinese(); 
       cn = new Chinese("codersai"); 
       cn = new Chinese("codersai", 18); 
    } 
}

运行结果如下:

父类·无参数构造方法: A Person.
子类·调用父类无参数构造方法“: A chinese coder.
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类含一个参数的构造方法“: his name is codersai
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类含一个参数的构造方法“: his name is codersai
子类:调用子类具有相同形参的构造方法:his age is 18

由此可见,可以用 superthis 分别调用父类的构造方法和本类中其他形式的构造方法。 Chinese 类第三种构造方法调用的是本类中第二种构造方法,而第二种构造方法是调用父类的,因此要先调用父类的构造方法,再调用本类中第二种,最后重写第三种构造方法。

访问控制

同C++一样,Java中,也可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持以下 4 种不同的访问权限:

修饰符 当前类 同一包内 子孙类(同一包) 子孙类(不同包) 其他包
public True True True True True
protected True True True 详见下文 False
default True True True False False
private True False False False False

默认访问修饰符 default

如果不写修饰符,则使用 default 作为修饰符,使用该访问修饰符声明的变量和方法,对同一个包内的类是可见的。接口里的变量都隐式声明为 public static final ,而接口里的方法默认情况下访问权限为 public

String version = "1.5.1";
boolean processOrder() {return true;}

私有访问修饰符 private

该修饰符不能修饰类(外部类),而使用该访问修饰符声明的变量和方法、只能被所属类访问,并且类和接口不能声明为 private 。如果要在外部类访问,只能通过其类提供的公共的 getter 方法被外部类访问,如下所示:

public class Logger {
    
    private String format; // 私有静态变量
    
    public String getFormat() { // 公共getter方法,用于外部类访问
        return this.format;
    }
    
    public void setFormat(String format) { // 公共setter方法,用于外部类修改
        this.format = format;
    }
}

实例中,Logger 类中的 format 变量为私有变量,所以其他类不能直接得到和设置该变量的值。为了使其他类能够操作该变量,定义了两个公共方法:getFormat()setFormat(String format)


公有访问修饰符 public

被声明为 public 的类、方法、构造方法和接口能够被任何其他类访问。如果几个相互访问的 public 类分布在不同的包中,则需要导入相应类所在的包。由于类的继承性,类所有的公有方法和变量都能被其子类继承。

public static void main(String[] arguments)

Java 程序的 main() 方法必须设置成公有的,否则解释器将不能运行该类。


受保护访问修饰符 protected

protected 需要从以下两个点来分析说明:

  • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
  • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected 方法。

protected 可以修饰数据成员,构造方法,方法成员,不能修饰类(内部类除外)

接口及接口的成员变量和成员方法不能声明为 protected 。 可以看看下图演示:

img

子类能访问 protected 修饰符声明的方法和变量,这样就能保护不相关的类使用这些方法和变量。

// 下面的父类使用了protected访问修饰符,子类重写了父类的openSpeaker()方法
class AudioPlayer {
   protected boolean openSpeaker(Speaker sp) {
      // 实现细节
   }
}
 
class StreamingAudioPlayer extends AudioPlayer {
   protected boolean openSpeaker(Speaker sp) {
      // 实现细节
   }
}

访问控制和继承

请注意以下方法继承的规则:

  • 父类中声明为 public 的方法在子类中也必须为 public
  • 父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private
  • 父类中声明为 private 的方法,不能够被子类继承。

static 关键字

在类中,使用 static 修饰符修饰的属性(成员变量)称为静态变量,也可以称为类变量,常量称为静态常量,方法称为静态方法或类方法,它们统称为静态成员,归整个类所有,随类一起被加载,因此不依赖于类的特定实例,被类的所有实例共享,就是说 static 修饰的方法或者变量不需要依赖于对象来进行访问,只要这个类被加载,Java 虚拟机就可以根据类名找到它们。

调用静态成员的语法形式如下:

类名.静态成员 // 如 Math.sqrt()

注意

  • static 修饰的成员变量和方法,从属于类,因此也称类变量和类方法,而普通变量和方法则从属于对象。
  • 静态变量只会加载一次,且存储在方法区的静态域中。
  • 静态方法不能调用非静态成员,因为加载类时,非静态成员不会随类一起加载,所以编译会报错。
  • static 还可以修饰代码块内部类,这些之后再说。

静态变量

类的成员变量可以分为以下两种:

  1. 静态变量(或称为类变量),指被 static 修饰的成员变量。
  2. 实例变量,指没有被 static 修饰的成员变量。

静态变量与实例变量的区别如下:

  1. 静态变量

    • 运行时,Java 虚拟机只为静态变量分配一次内存,在加载类的过程中完成静态变量的内存分配。
    • 在类的内部,可以在任何方法内直接访问静态变量。
    • 在其他类中,可以通过类名访问该类中的静态变量。
  2. 实例变量

    • 每创建一个实例,Java 虚拟机就会为实例变量分配一次内存。
    • 在类的内部,可以在非静态方法中直接访问实例变量。
    • 在本类的静态方法或其他类中则需要通过类的实例对象进行访问。

静态变量在类中的作用如下:

  • 静态变量可以被类的所有实例共享,因此静态变量可以作为实例之间的共享数据,增加实例之间的交互性。
  • 如果类的所有实例都包含一个相同的常量属性,则可以把这个属性定义为静态常量类型,从而节省内存空间。
public static double PI = 3.14159256; // 例如,在类中定义一个静态常量 PI

如下是一个对静态变量的实例:

// 创建一个带静态变量的类,然后在main()方法中访问该变量并输出结果
public class StaticVar {
    public static String str1 = "Hello";
    public static void main(String[] args) {
        String str2 = "World!";
        // 直接访问str1
        String accessVar1 = str1+str2;
        System.out.println("第 1 次访问静态变量,结果为:"+accessVar1);
        // 通过类名访问str1
        String accessVar2 = StaticVar.str1+str2;
        System.out.println("第 2 次访问静态变量,结果为:"+accessVar2);
        // 通过对象svt1访问str1
        StaticVar svt1 = new StaticVar();
        svt1.str1 = svt1.str1+str2; // 注意,此处类的静态变量str1已经被赋值为"HelloWorld!",对所有实例生效
        String accessVar3 = svt1.str1;
        System.out.println("第3次访向静态变量,结果为:"+accessVar3);
        // 通过对象svt2访问str1
        StaticVar svt2 = new StaticVar();
        String accessVar4 = svt2.str1+str2; // 因此,accessVar4 = "HelloWorld!" + "World!"
        System.out.println("第 4 次访问静态变量,结果为:"+accessVar4);
    }
}

运行结果如下:

第 1 次访问静态变量,结果为:HelloWorld!
第 2 次访问静态变量,结果为:HelloWorld!
第 3 次访向静态变量,结果为:HelloWorld!
第 4 次访问静态变量,结果为:HelloWorld!World!

由此可见,在类中定义静态属性(成员变量),在 main() 方法中能直接访问,也能通过类名访问,还能通过类的实例对象来访问。

注意:静态变量是被多个实例所共享的,就像运行结果的第四行输出一样。


静态方法

与成员变量类似,成员方法也可以分为以下两种:

  1. 静态方法(或称为类方法),指被 static 修饰的成员方法。
  2. 实例方法,指没有被 static 修饰的成员方法。

静态方法与实例方法的区别如下:

  • 静态方法不需要通过它所属的类的任何实例就可以被调用,因此在静态方法中不能使用 this 关键字,也不能直接访问所属类的实例变量和实例方法,但是可以直接访问所属类的静态变量和静态方法。另外,和 this 关键字一样,super 关键字也与类的特定实例相关,所以在静态方法中也不能使用 super 关键字。

  • 在实例方法中可以直接访问所属类的静态变量、静态方法、实例变量和实例方法。

如下是一个对静态方法的实例:

// 创建一个带静态变量的类,添加几个静态方法对静态变量的值进行修改,然后在main()方法中调用静态方法并输出结果
public class StaticMethod {
    public static int count = 1;    // 定义静态变量count
    public int method1() {    
        // 实例方法method1
        count++;    // 访问静态变量count并赋值
        System.out.println("在静态方法 method1()中的 count="+count);    // 打印count
        return count;
    }
    public static int method2() {    
        // 静态方法method2
        count += count;    // 访问静态变量count并赋值
        System.out.println("在静态方法 method2()中的 count="+count);    // 打印count
        return count;
    }
    public static void PrintCount() {    
        // 静态方法PrintCount
        count += 2;
        System.out.println("在静态方法 PrintCount()中的 count="+count);    // 打印count
    }
    public static void main(String[] args) {
        StaticMethod sft = new StaticMethod();
        // 通过实例对象调用实例方法
        System.out.println("method1() 方法返回值 intro1="+sft.method1());
        // 直接调用静态方法
        System.out.println("method2() 方法返回值 intro1="+method2());
        // 通过类名调用静态方法,打印 count
        StaticMethod.PrintCount();
    }
}

运行结果如下:

在静态方法 method1()中的 count=2
method1() 方法返回值 intro1=2
在静态方法 method2()中的 count=4
method2() 方法返回值 intro1=4
在静态方法 PrintCount()中的 count=6

在该程序中,静态变量 count 作为实例之间的共享数据,因此在不同的方法中调用 count,值是不一样的。从该程序中可以看出,在静态方法 method1()PrintCount() 中是不可以调用非静态方法 method1() 的,而在 method1() 方法中可以调用静态方法 method2()PrintCount()

在访问非静态方法时,需要通过实例对象来访问,而在访问静态方法时,能直接访问,也能通过类名来访问,还能通过实例对象来访问。

final 关键字

final 在 Java 中常用来表示常量,且应用于类、方法和变量时意义是不同的,但本质是一样的,都表示不可改变

注意事项:

  • final 修饰的变量的值不可以改变,此时该变量可以被称为常量
  • final 修饰的方法不可以被重写
  • final 修饰的类不可以被继承

修饰变量

final修饰的变量即成为常量,只能赋值一次,但是所修饰局部变量和成员变量有所不同。

  1. final 修饰的局部变量必须使用之前被赋值一次才能使用。
  2. final 修饰的成员变量在声明时没有赋值的叫“空白 final 变量”。空白 final 变量必须在构造方法或静态代码块中初始化。

注:final 修饰的变量不能被赋值这种说法是错误的,严格的说法是 final 修饰的变量不可被改变,一旦获得初始值,该变量就不能被重新赋值。

// 若是成员变量仅声明不赋值,则必须在构造方法或静态代码块中初始化
final int e; e = 100; // 只能赋值一次
final static int f = 200; // 常见用法,可加修饰符

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

final int[] iArr = {5, 6, 12, 9};
Arrays.sort(iArr); // 对数组元素进行排序,合法
iArr[2] = -8;      // 对数组元素赋值,合法
iArr = null;       // 对iArr重新赋值,非法

final Person person = new Person(45); // 构造器参数用来初始化年龄
person.age = 18; // 对属性进行操作,合法
person = null;   // 对person重新赋值,非法

注:使用 final 声明的常量,一般要求全部的字母大写,如 SEX,这点在开发中是非常重要的。


修饰方法

final 修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用 final 修饰该方法。

如果子类中定义一个与父类 private 方法同名、同形参列表、同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以在其子类中“重写”该方法。

public class PrivateFinalMethodTest {
    private final void test() {...}
}

class Sub extends PrivateFinalMethodTest {
    public void test() {...} // 没有问题,因为不算重写
}

修饰类

final 修饰的类不能被继承。当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可被继承,则可以使用 final 修饰这个类。

下面代码示范了 final 修饰的类不可被继承。

final class SuperClass {...}
class SubClass extends SuperClass {...} // 编译出错

因为 SuperClass 类是一个 final 类,而 SubClass 试图继承 SuperClass 类,这将会引起编译错误。

可变长参数

Java 从 JDK1.5 以后,允许定义形参长度可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时在最后一个形参类型后增加3个点即(...),则表明该形参可以接受多个参数值,多个参数值会被当做数组传入。

public class Test {
	public static void main(String[] args) {
		par("张", "陈", "刘");
	}

	public static void par(String... strings) { // 声明可变长参数,其实就是数组
		for (String s : strings) {
			System.out.print(s);
		}
	}
}

注意事项:

  • 调用时,如果同时能匹配固定参数和可变长参数的方法,会优先匹配固定参数方法。

  • 如果能同时和2个包含可变参数的方法想匹配,则编译会报错,因为编译器不知道该调用哪个方法。

  • 一个方法只能有一个可变参数,且可变参数应为最后一个参数。

方法重载

类的特性之一,与C++一样,在Java中,你可以通过在一个类中定义多个同名的方法,且每个方法具有不同的参数类型参数个数。这样一来在调用方法时通过传递给它们的不同个数和类型的参数,以及传入参数的顺序来就可以决定具体使用哪个方法。方法重载提高了程序的兼容性和可读性,使程序可以处理多种情况。

如下代码所示:

public class TestMax {
    // 主方法
    public static void main(String[] args) {
        int i = 5;
        int j = 2;
        int k = max(i, j);
        System.out.println( i + " 和 " + j + " 比较,最大值是:" + k);
    }
 
    // 返回两个整数变量较大的值
    public static int max(int num1, int num2) {
        int result;
        if (num1 > num2)
           result = num1;
        else
           result = num2;
        return result; 
    }
    
    // max方法重载,使其可以处理浮点数
    public static double max(double num1, double num2) { // 同名且具有不同的参数类型
        if (num1 > num2)
            return num1;
        else
            return num2;
    }
}

注:重载的方法必须拥有不同的参数列表,你不能仅仅依据修饰符或返回值类型的不同来重载方法。

虚拟方法

Java 多态性的体现之一,具体用法是将用子类创建的实例绑定到用父类声明的引用变量上,如下:

Father name = new Son() // Father为父类,Son为子类,name为引用变量名;必须要有继承关系!

虽然用的是子类的构造器创建,但 name 只能访问父类所拥有的属性和方法,且如果子类该方法有重写,则会访问子类重写后的方法,看起来没啥用,实际上提高代码的兼容性,具体用法如下:

public class Test {
    public static void main(String[] args) {
        action(new Fuck()); // 相当于 Animal animal = new Fuck()
        action(new Shit()); // 相当于 Animal animal = new Shit()
    }
    
    public static void action(Animal animal) {
        animal.shout(); // 这里调用的是子类重写的方法
    }
}

class Animal {
    public void shout() {
        System.out.println("叫~");
    }
}

class Fuck extends Animal {         // 继承
    public void shout() {           // 重写
        System.out.println("Fuck!");
    }
}

class Shit extends Animal {
    public void shout() {           // 同上
        System.out.println("Shit!");
    }
}

运行结果如下:

Fuck!
Shit!

如果没有虚拟方法,则需要对上面代码中的的 action 方法进行重载,分别修改参数类型为 FuckShit 才能使用,那样过于冗杂。

代码块

在 Java 中,使用 {} 括起来的代码被称为代码块(Code block),根据其位置声明的不同,可以分为:

  • 局部代码块:常在方法中出现,可以限定变量生命周期,及早释放,提高内存利用率
  • 构造代码块:在类中方法外出现的局部代码块,每次调用构造方法都会执行,并且在构造方法前执行
  • 同步代码块:指被 Synchronized 修饰的代码块,这是一种线程同步机制,被该关键词修饰的代码块会被加上内置锁
  • 静态代码块:在类中方法外出现,并加上 static 修饰,常用于给类进行初始化,在加载的时候就执行,且静态代码块只执行一次

同步代码块属于多线程部分,此处先不展示,其余代码块示例如下:

class StatisCodeBlock {
    static { // 静态代码块,在方法外出现
        int number1 = 10;
        System.out.println("1、静态代码块变量: " + number1);
    }

    { // 构造代码块,在方法外出现
        int number2 = 20;
        System.out.println("2、构造代码块变量: " + number2);
    }

    public StatisCodeBlock() { // 构造方法
        { // 局部代码块
            int number5 = 50; // 局部中的局部变量,生命周期仅限代码块中
            System.out.println("5、局部代码块变量: " + number5);
        }
        System.out.println("这是构造方法 StatisCodeBlock()");
    }

    static { // 静态代码块按照声明先后顺序执行
        int number3 = 30;
        System.out.println("3、静态代码块变量: " + number3);
    }

    { // 构造代码块也按照声明先后顺序执行,且构造代码块先于构造方法执行
        int number4 = 40;
        System.out.println("4、构造代码块变量: " + number4);
    }
}

public class CodeBlockTest {
    public static void main(String[] args) {
        StatisCodeBlock codeBlock = new StatisCodeBlock(); // 创建对象
        System.out.println("======第二次创建实例======"); // 注意:构造代码块通过构造方法自动调用
        StatisCodeBlock codeBlock2 = new StatisCodeBlock();
    }
}

运行结果如下:

1、静态代码块变量: 10
3、静态代码块变量: 20
2、构造代码块变量: 30
4、构造代码块变量: 40
5、局部代码块变量: 50
这是构造方法 StatisCodeBlock()
======第二次创建实例======
2、构造代码块变量: 20
4、构造代码块变量: 40
5、局部代码块变量: 50
这是构造方法 StatisCodeBlock()

包装类

包装类位于 java.lang 包中,是为了解决 Java 中八种基本数据类型不是面向对象的问题,而为每种基本数据类型设计的一个对应的类,分别为 ByteShortIntegerLongFloatDoubleBooleanCharacter。它可以将基本数据类型转换为对象,也可以将对象转换为基本数据类型。自 JDK 5.0 以来,Java 支持自动装箱自动拆箱的功能,即可以自动地将基本数据类型转换为包装类对象,或者将包装类对象转换为基本数据类型


常用方法

方法名 说明
valueOf 将基本数据类型或字符串转换为包装类对象。
parseXxx 将字符串转换为基本数据类型
toString 将包装类对象转换为字符串。
xxxValue 将包装类对象转换为基本数据类型
compareTo 比较两个包装类对象的大小
equals 判断两个包装类对象是否相等

示例如下:

//自动装箱和自动拆箱
Integer i = 10; //将int类型的10自动装箱为Integer对象
int j = i; //将Integer对象i自动拆箱为int类型的j

//valueOf()方法
Integer i1 = Integer.valueOf(10); //将int类型的10转换为Integer对象
Integer i2 = Integer.valueOf("10"); //将字符串"10"转换为Integer对象

//parseXxx()方法
int i3 = Integer.parseInt("10"); //将字符串"10"转换为int类型的i3
double d1 = Double.parseDouble("3.14"); //将字符串"3.14"转换为double类型的d1

//toString()方法
String s1 = i1.toString(); //将Integer对象i1转换为字符串s1
String s2 = d1.toString(); //将double类型的d1转换为字符串s2

//xxxValue()方法
int i4 = i1.intValue(); //将Integer对象i1转换为int类型的i4
double d2 = i1.doubleValue(); //将Integer对象i1转换为double类型的d2

//compareTo()方法
int c1 = i1.compareTo(i2); //比较Integer对象i1和i2的大小,返回0表示相等,返回正数表示i1大于i2,返回负数表示i1小于i2
int c2 = d1.compareTo(d2); //比较double类型的d1和d2的大小,返回0表示相等,返回正数表示d1大于d2,返回负数表示d1小于d2

//equals()方法
boolean b1 = i1.equals(i2); //判断Integer对象i1和i2是否相等,返回true表示相等,返回false表示不相等
boolean b2 = d1.equals(d2); //判断double类型的d1和d2是否相等,返回true表示相等,返回false表示不相等

内部类

Java 一个类中可以嵌套另外一个类,叫做内部类,它享有作为一个类所拥有的大部分功能,语法格式如下:

class OuterClass {   // 外部类
    class NestedClass {...}// 嵌套类,或称为内部类
}

成员内部类

成员内部类是定义在方法外的内部类,作为外部类的成员,它同时还享有作为一个成员所拥有的大部分功能

成员内部类可以使用修饰符,这决定了成员内部类和外部类相互之间的访问权限以及需不需要创建外部类来访问成员内部类

class OuterClass {
    int x = 10; // 外部实例变量
    class InnerClass1 {int y = 1;} // 公有内部类
    private class InnerClass2 {int y = 2;} // 私有内部类
    static class InnerClass3 {int y = 3;} // 静态内部类
}

public class MyMainClass {
    public static void main(String[] args) {
        OuterClass myOuter = new OuterClass(); // 必须先实例化外部类,才能访问非静态内部类
        OuterClass.InnerClass1 myInner1 = myOuter.new InnerClass1();
        System.out.println(myInner1.y + myOuter.x); // 输出结果为:15
     // OuterClass.InnerClass2 myInner2 = myOuter.new InnerClass2(); 编译报错,因为外部类无法访问私有内部类
        OuterClass.InnerClass3 myInner3 = new OuterClass.InnerClass3(); // 无需创建外部类,可以直接访问静态内部类
        System.out.println(myInner3.y); // 输出结果为:3 
    }
}

注:静态内部类无法访问外部类的成员,因为外部类还没加载


局部内部类

局部内部类是定义在方法内,代码块内,构造器内的内部类,比较少见,下面是一个比较常见的用法:

// 返回一个是实现Test接口的类的对象
public Test getTest() {
    class MyTest implements Test {...} // 创建一个实现Test接口的类:局部内部类
    return new MyTest(); // 返回使用虚拟方法 Test ? = new MyTest()
}

注意事项:

  • 局部内部类不能使用访问控制修饰符和 static 关键字修饰,也不能定义 static 成员
  • 局部内部类只在当前方法中有效
  • 局部内部类中还可以包含内部类,不过这些类也不能用一样的修饰符
  • 局部内部类中可以访问外部类的所有成员
  • 局部内部类中只可以访问当前方法中 final 类型的参数与变量

抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

在 Java 语言中使用 abstract 关键字来定义抽象类和方法。如下:

public abstract class Test {          // 抽象类
    public abstract void fuck(int x); // 抽象方法,没有方法体,由继承的子类来重写实现
    public void shit(int x) {...}     // 非抽象方法
}

要点:

  • 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样

  • 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用

  • 含有抽象方法的一定是抽象类,反之则不一定

  • 若抽象类含有抽象方法,则继承的子类必须重写从父类及更高辈分的类继承下来的抽象方法,除非子类也是抽象类

  • 属性、代码块、构造方法、private 方法、static 方法、final 方法和类,不能声明为抽象

接口

接口在 Java 中是一个抽象类型,并不是类,是抽象方法的集合,接口通常以 interface 关键字来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法,除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

[public] interface Name [extends ...] { // 接口可以加其他修饰符,且支持多继承,不必使用abstract关键字
    [public static final] int x = 1; // 全局常量
    [public abstract] void setName(String name); // 抽象方法,没有方法体
}

要点:

  • 接口不能实例化且只有公有的抽象方法,必须被继承实现。继承接口的类必须实现接口内所有抽象方法,否则就必须声明为抽象类
  • 接口中,除默认方法和静态方法,其余方法会被隐式的指定为 public abstract ,除 private 其他修饰符都会报错
  • 接口中的变量会被隐式的指定为 public static final ,其他修饰符都会报错
  • 接口不能包含成员变量,除了 staticfinal 变量
  • Java 8 之后可以使用 default 关键字在接口中修饰非抽象方法

接口的实现和继承

接口的实现,继承,方法实现与抽象类类似,可以参考新特性部分的代码,这里不再赘述,只提几点注意事项:

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
  • 接口可以多继承

新特性

:JDK 1.8 以后,接口里可以有静态方法方法体
:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",使用 default 关键字修饰。可参考 Java 8 默认方法
:JDK 1.9 以后,允许将方法定义为 private,使某些复用的代码不会把方法暴露出去。可参考 Java 9 私有接口方法

示例如下:

public interface Test {
    int x = 1; // 全局常量
    public void method1(); // 抽象方法
    public static void method2() { // 静态方法,只能通过 Test.method2 来调用
        System.out.print("Hello");
    }
    public default void method3() { // 默认方法
        System.out.print("World");
    }
}
public class Main implements Test {
    public void method1() { // 实现抽象方法
        Test.method2(); // 调用接口的静态方法
    }
    public void method3() { // 重写默认方法
        Test.super.method3(); // 重写的前提下,调用接口的默认方法
    }
}

注意事项:

  • 接口中的静态方法只能通过接口来调用,实现类无法访问
  • 接口中的默认方法可以通过实现类的对象来调用,若该方法被重写,则调用的是重写后的方法
  • 如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法*(类优先原则)*
  • 如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,编译器将会报错,所以只能通过方法重写解决*(接口冲突)*
  • 如果想在已经重写的情况下,调用接口的默认方法,可以使用 接口名.super.方法名 来调用