主要来自于《尚硅谷Java教程》

目录

类的属性和方法

面向过程与面向对象

二者之间的主要区别:

  • 面向过程(POP)强调的是功能行为,以函数为最小单位,考虑怎么做。
  • 面向对象(OOP)将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。

面向对象的三大特征:

  • 封装(Encapsulation)
  • 继承(Inheritance)
  • 多态(Polymorphism)

内存解析

  • 堆(Heap),此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • 通常所说的栈(Stack),是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference类型它不等同于对象本身,是对象在堆内存的首地址)。方法执行完,自动释放。
  • 方法区(MethodArea),用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

属性(非static)加载在堆空间中,而局部变量加载在栈空间中。

方法重载与可变形参

方法的重载(overload):

  • 概念:在同一个类中,允许存在一个以上的同名算法,只要他们的参数个数或者参数类型不同即可。
  • 特点:与返回值类型无关,只看参数列表,且参数列表必须不同(参数的个数或类型)。

JDK 5.0提供了Varargs(variable number of arguments)机制,允许直接定义能和多个实参相匹配的形参。

  • 格式:String ... strs
  • 传入的参数个数可以是:0, 1, 2, ...。
  • 可变个数形参的方法和与本类中的同名且形参不同方法构成重载。
  • 可变个数形参的方法和与本类中的同名且形参类型相同的数组之间不构成重载。
public static void show(String str){
    System.out.println(str);
}

// 构成重载
public static void show(String ... strs){
    for (String str : strs) {
        System.out.print(str + ' ');
    }
    System.out.println();
}

// 与可变个数形参冲突,不构成重载
//    public static void show(String[] strs){
//        for (String str : strs) {
//            System.out.print(str + ' ');
//        }
//        System.out.println();
//    }
  • 可变个数形参必须声明在末尾。
  • 最多只能声明一个可变形参。

方法值传递机制

方法,必须由其所在类或对象调用才有意义。若方法含有参数:

  • 形参:方法声明时的参数。
  • 实参:方法调用时实际传给形参的参数值

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

  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参。
  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参。

理解变量的赋值

  • 如果变量是基本数据类型,此时赋值的是变量保存的数据值。
  • 如果变量是引用数据类型,此时赋值的是变量所保存数据的地址。
public class ValueTransferTest {
    public static void main(String[] args){
        int a = 1;
        int b = a;  // 赋值的是变量保存的数据值1
        b = 2;

        Transfer tran1 = new Transfer();
        Transfer tran2 = tran1;  // 赋值变量所保存数据的地址
        tran2.num = 5;

        System.out.println("" + a + " " + b + " " + tran1.num + " " + tran2.num);
        // 1 2 5 5
    }
}

class Transfer {
    int num;
}

类似的:

  • 如果参数是基本数据类型,此时实参赋給形参的是实参真实存储的数据值
  • 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值

四种权限修饰

Java规定的四种权限修饰(从小到大):private<缺省<protected<public。四种权限都可以用来修饰累的内部结构:属性方法、构造器、内部类。通过权限修饰来体现封装性。

修饰符 类内部 同一个包 不同包的子类 同一个工程
private Yes
(缺省) Yes Yes
protected Yes Yes Yes
public Yes Yes Yes Yes
  • 对于class的修饰只可以是public或缺省。

构造器

构造器(constructor)也称为构造方法,用来在创建对象、初始化对象。

  • 如果没有显式的定义类的构造器的话,则系统默认提供一个空参的构造器,所以一个类中,至少会有一个构造器。
  • 定义构造器的格式:权限修饰符类名(形参列表){ }。
  • 一个类中定义的多个构造器,彼此构成重载。
  • 一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器。

属性初始化顺序

属性初始化的先后顺序如下:

  1. 默认初始化,例如private int age;默认初始化为0
  2. 显式初始化,例如private int age = 18;。或者代码块中赋值,二者先后顺序为声明顺序。
  3. 构造器中初始化。
  4. 通过方法给属性赋值,例如setAge(30);

JavaBean

JavaBean是一种Java语言写成的可重用组件。所谓javaBean,是指符合如下标准的Java类:

  • 类是公共的。
  • 有一个无参的公共的构造器。
  • 有属性,且有对应的get、set方法。

用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

this的使用

关键字this可以用来修饰:属性、方法、构造器。

属性与方法

  • this用来修饰属性和方法时,this可以理解为当前对象,在类的方法中,可以通过this.来调用当前对象的属性和方法。通常情况下,省略this.,但当方法的形参和类的属性同名时,必须显式使用this.。例如:
public void setAge(int age) {
    this.age = age;
}

构造器

使用this来调用其他的构造器(不能调用自己)。例如:

public Person() {
    age = 18;
}

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

public Person(String name, String sex) {
    this(name);
    this.sex = sex;
}
  • 如果一个类中有n个构造器,最多有n-1个构造器通过this调用其他构造器。
  • this调用其他构造器必须声明在构造器首行,且最多只能声明一个。

package和import的使用

package的使用

  1. 为了更好的实现项目中类的管理,提供包的概念
  2. 使用package声明类或接口所属的包,声明在源文件的首行
  3. 包属于标识符,遵循标识符的命名规则、规范( xxxyyyzzz)、“见名知意”
  4. .一次,就代表一层文件目录,例如com.learnjava.arraytest

JDK中主要的包

  1. java.lang:包含一些Java语言的核心类,如StringMathIntegerSystemThread,提供常用功能。
  2. java.net:包含执行与网络相关的操作的类和接口。
  3. java.io:包含能提供多种输入输出功能的类。
  4. java.util:包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
  5. java.text:包含了一些java格式化相关的类。
  6. java.sql:包含了java进行JDBC数据库编程的相关类/接口。
  7. java.awt:包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。

import关键字

  • 使用的类或接口是java.lang包,或者本包下定义的,则可以省略import
  • 如果源文件中,使用了不同包下的同名类(如java.util.Datejava.sql.Date),则必须有一个要以全名的方式显示,如下所示:
import java.util.Date;

public class ImportTest {
    public static void main(String[] args) {
        Date date = new Date();
        java.sql.Date date1 = new java.sql.Date(123123123123132L);  // 全名则不需要import
    }
}
  • 使用类似import java.util.*;的方式,可以调用包下的所有结构,但是如果使用的是其子包下的类,如java.util.zip.CRC32,则仍需要显式import,如下所示:
import java.util.*;
import java.util.zip.CRC32;


public class ImportTest {
    public static void main(String[] args) {
        new CRC32();  // 需要单独import
    }
}
  • 使用import static来导入指定类或接口中的静态结构,如下所示:
import static java.lang.Math.*;

public class ImportTest {
    public static void main(String[] args) {
        System.out.println(pow(PI, 2));  // 输出pi的平方
    }
}

代码块的使用

  • 代码块用来初始化类或对象,只能用static修饰。
  • 根据是否用static修饰,分为静态代码块和非静态代码块。
import org.junit.Test;

public class BlockTest {
    @Test
    public void testBlock() {
        Person p1 = new Person();
        // Static block.
        // Block.

        Person p2 = new Person();
        // Block.

        Person.info();
        // I am a happy person!
    }
}

class Person {
    // 属性
    String name;
    int age;
    static String describe = "I am a Person.";

    // 构造器
    public Person() {
    }

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

    // 静态代码块
    static {
        System.out.println("Static block.");
        describe = "";
    }

    // 非静态代码块
    {
        System.out.println("Block.");
        age = 1;
    }

    public void eat() {
        System.out.println("Person eat.");
    }

    public static void info() {
        System.out.println("I am a happy person!");
    }
}

静态代码块

  1. 内部可以有输出语句。
  2. 随着类的加载而执行,而且只执行一次
  3. 作用:初始化类的信息。
  4. 如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行。
  5. 静态代码块的执行要优先于非静态代码块的执行。
  6. 静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构。

非静态代码块

  1. 内部可以有输出语句。
  2. 随着对象的创建而执行。每创建一个对象,就执行一次非静态代码块。
  3. 作用:可以在创建对象时,对对象的属性等进行初始化。
  4. 如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行。
  5. 非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法。
继承性

继承的使用

继承(inheritance)的格式:class A extends B{}

  • A:子类、派生类、subclass。
  • B:父类、超类、superclass。

一旦A继承B之后,A就获得了B中所有的属性和方法。特别地,父类中声明为private的属性或方法,子类仍然能够获取,只是因为封装性的原因,子类无法直接调用父类的私有结构。

继承的特点

  1. Java中类的单继承性:一个类可以被多个子类继承,但一个类只能有一个父类。
  2. 子类父类是相对的概念,子类直接继承的父类称为直接父类,间接继承的父类称为简介父类。
  3. 子类继承父类后,就获取了直接父类和所有间接父类中声明的属性和方法。

方法的重写

重写(override):在子类中根据需要对从父类中继承的方法进行改造,在程序执行时,子类的方法将覆盖父类的方法。

public class OverrideTest {
    public static void main(String[] args) {
        new Person().eat();  // Person eat.
        new Student().eat();  // Student eat.
    }
}

class Person{
    public void eat() {
        System.out.println("Person eat.");
    }
}

class Student extends Person {
    @Override  // 告诉编译器这是一个重写的方法,需要检查,如果不符合重写规定(如写成eat1)会报错
    public void eat() {
        System.out.println("Student eat.");
    }
}
  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表。
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。
    • 如果父类被重写的方法返回值类型是void,则子类重写的方法也必须返回void
    • 如果父类被重写的方法返回值类型是A类,则子类重写的方法也必须返回A类或其子类。
    • 如果父类被重写的方法返回值类型是基本数据类型,如double,则子类重写的方法也必须返回相同的数据类型,double
  • 子类重写的方法使用的访间权限不能小于父类被重写的方法的访间权限。
  • 子类不能重写父类中声明为private权限的方法。
  • 子类方法抛出的异常类型不能大于父类被重写方法的异常类型。

最后需要注意,子类和父类中的同名同参数方法要么都声明为非static的(考虑重写),要么都声明为static(不是重写)。

super关键字

super理解为父类的,用来调用:属性、方法、构造器。super的使用:

  1. 在子类的方法和构造器中,显式调用父类中声明的属性或方法,但一般省略super.
  2. 特殊情况:当子类和父类定义了同名的属性时,必须显式调用父类中声明的属性。
  3. 特殊情况:当子类重写了父类中的方法后,则需要显式调用父类中被重写的方法。

super调用构造器

  1. 通过显式的使用super()的方式来调用父类中声明的指定构造器。
  2. super()必须声明在子类构造器的首行,所以需要和this()二选一。
  3. 如果没有显示声明super()this(),则默认调用父类中空参数的构造器。

子类对象实例化过程

从结果上看(继承性)

  • 子类继承父类以后,就获取了父类中声明的属性或方法。
  • 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

从过程上看

  • 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止。
  • 正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。

Object类

  1. 如果没有显式声明一个类的父类,则此类继承于java.lang.Object类。
  2. 所有的类都直接或间接继承自java.lang.Object类(除了它本身),所以所有类都具有java.lang.Object类所声明的功能。
  3. Object类只声明了一个空参的构造器。

equals方法

首先,回顾==运算符的用法:

  • 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等(不一定类型要相同)。
double a = 1;
float b = 1;
int c = 1;
System.out.println(a == b);  // true
System.out.println(b == c);  // true
  • 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。

与==运算符的区别

对于equals方法与==运算符的区别,主要注意的有:

  • equals是方法,而非运算符。
  • 只适用于引用数据类型。
  • Object类中equals方法的定义和==运算符相同。
public boolean equals(Object obj) {
    return (this == obj);
}
  • StringDataFile、包装类等都重写了Object类中的equals()方法,重写后,比较的不是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同。
  • 手动重写equals方法,如下所示(由IDEA自动生成):
import java.util.Objects;

public class ObjectTest {
    public static void main(String[] args) {
        Animal a1 = new Animal("name1", 10);
        Animal a2 = new Animal("name1", 10);
        System.out.println(a1 == a2);  // false
        System.out.println(a1.equals(a2));  // true
    }
}

class Animal {
    private String name;
    private int age;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Animal animal = (Animal) o;
        return age == animal.age && Objects.equals(name, animal.name);
    }
}

上面的Objects.equals(Object a, Object b)方法源码如下:

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

重写equals原则

  1. 对称性:x.equals(y)y.equals(x)返回结果相同。
  2. 自反性:x.equals(x)返回true
  3. 传递性:如果x.equals(y)y.equals(z)都返回true,则x.equals(z)返回true
  4. 一致性:只要xy的内容不变,不管执行x.equals(y)多少次,返回结果都应相同。
  5. 任何情况下,x.equals(null)返回falsenull.equals(x)返回false
  6. x.equals(和x不同的类的对象)返回false

toString方法

  • 当我们输出一个对象的引用时,实际上就是调用当前对象的toString方法。在Object类中toString方法输出类名和地址值,其定义如下:
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • StringDataFile、包装类等都重写了Object类中的toString()方法。
多态性

多态的使用

对象的多态性:父亲的引用指向子类的对象。多态性只适用于方法,不适用于属性。

  • 在编译器,只能调用父类声明的方法,但是在运行期,实际运行的是子类重写父类的方法。

多态使用的前提:

  1. 需要有类的继承关系。
  2. 方法的重写。
public class OverrideTest {
    public static void main(String[] args) {
        Person p1 = new Student();
        Person p2 = new Teacher();

        p1.eat();  // Student eat.
        p2.eat();  // Teacher eat.
        // p1.study();  // 不能调用子类的方法,因为上面声明了Person p1
    }
}

class Person{
    public void eat() {
        System.out.println("Person eat.");
    }
}

class Student extends Person {
    @Override
    public void eat() {
        System.out.println("Student eat.");
    }

    public void study() {
        System.out.println("Student study.");
    }
}

class Teacher extends Person {
    @Override
    public void eat() {
        System.out.println("Teacher eat.");
    }
}

instanceof关键字

instanceof使用

关键字a instanceof A用来判断对象a是否属于指定类A

  • 如果a instanceof A返回trueBA的父类,则a instanceof B也返回ture

向下转型与向上转型

父类(Person)为上,子类(Student)为下。

  • 向上转型:即多态,如Person p1 = new Student();
  • 向下转型:强制类型转换,需要使用instanceof关键字进行判断。

有了多态后,内存实际上加载了子类特有的属性和方法,但是由于声明为父类,编译时无法调用。此时需要使用强制类型转换符向下转型。如下所示:

public class OverrideTest {
    public static void main(String[] args) {
        Person p1 = new Student();
        Person p2 = new Teacher();

        p1.eat();  // Student eat.
        p2.eat();  // Teacher eat.
        // p1.study();  // 不能调用子类的方法,因为上面声明了Person p1
        
        if (p1 instanceof Student) {
            ((Student) p1).study();  // Student study.
        }

        if (p2 instanceof Student) {  // false
            ((Student) p2).study();
        }
    }
}
单元测试

步骤:

  1. 创建Java类进行测试,要求:此类是public的,且提供公共的无参构造器。
  2. 在此类中声明单元测试方法,要求:方法的权限是public的,没有形参,没有返回值。
  3. 在单元测试方法上声明注解@Test,并在单元测试类中导入import org.junit.Test;
  4. 声明好之后,在方法体内测试相关的代码。
import org.junit.Test;
import java.util.Date;

public class JUnitTest {
    @Test
    public void testPerson() {
        Object s1 = "test";
        String s2 = "test";
        System.out.println(s1.equals(s2));  // Test passed
        
        // Date date = (Date) s1;  // 如果运行,则出现Tests failed
    }
}
包装类

包装类(Wrapper)指针对8种基本数据类型定义的相应的引用类型,一般是基本数据类型首字母大写,有2个例外:IntegerCharacter

基本数据类型和包装类之间的转换

基本数据类型=>包装类

Integer i1 = new Integer(123);
Integer i2 = new Integer("55");

Float f1 = new Float(12.3);

Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("true");
Boolean b3 = new Boolean("true123");  // Boolean比较特殊,不会报错
System.out.println(b3);  // false
  • 需要注意的是,如果类中有属性是Boolean类型的,由于是引用类型,它的默认初始化值不再是false,而是null

包装类=>基本数据类型

Integer i1 = new Integer(123);
int i = i1.intValue();

自动装箱与与拆箱

// 自动装箱
int num = 10;
Integer i = num;

// 自动拆箱
int num2 = i;

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

  • 基本数据类型与包装类转换成String类型有以下两种方法:
// 方式1:连接运算
int num = 20;
String str = num + "";

// 方式2:调用valueOf方法
float f1 = 13.5f;
String str1 = String.valueOf(f1);

Double d1 = 3.14;
String str2 = String.valueOf(d1);
  • String类型转基本数据类型使用parseInt等方法(可能报错):
int num = Integer.parseInt("12345");
// int num2 = Integer.parseInt("123.4");  // NumberFormatException
boolean b1 = Boolean.parseBoolean("true1");  // false
static关键字

我们有时候希望无论产生了多少对象,某些特定的数据在内存空间里只有一份

static修饰属性

属性按照是否被static修饰分为静态属性和实例变量。当创建了类的多个对象,多个对象共享一个静态变量。

public class StaticTest {
    public static void main(String[] args) {
        Chinese c1 = new Chinese();
        Chinese c2 = new Chinese();

        c1.nation = "China";
        System.out.println(c2.nation);  // China
        System.out.println(Chinese.nation);  // China
    }
}

class Chinese {
    static String nation;
}
  • 静态变量随着类的加载而加载,可以直接通过类名调用。
  • 静态变量的加载早于对象的创建,由于类只会加载一次,静态变量在内存中也只会存在一份:存在方法区静态域中。

static修饰方法

  • 静态方法中只能调用静态的属性或方法。
  • 静态方法内,不能使用thissuper关键字。

单例设计模式

设计模式

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。

单例设计模式概念

所谓类的单例(Singleton)设计模式,就是采取一定的方法保证在整个的软件系统中:

  • 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
  • 首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。
  • 因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

单例模式实现

饿汉式

public class SingletonTest {
    public static void main(String[] args) {
        Bank b1 = Bank.getInstance();
        Bank b2 = Bank.getInstance();
        System.out.println(b1 == b2);  // true
    }
}

// 饿汉式
class Bank {
    // 1. 私有化类的构造器
    private Bank() {
    }

    // 2. 内部创建静态的类的对象
    private static Bank instance = new Bank();

    // 3. 提供公共的静态方法,返回类的对象
    public static Bank getInstance() {
        return instance;
    }
}

懒汉式

public class SingletonTest {
    public static void main(String[] args) {
        Order o1 = Order.getInstance();
        Order o2 = Order.getInstance();
        System.out.println(o1 == o2);  // true
    }
}

// 懒汉式
class Order {
    // 1. 私有化类的构造器
    private Order() {
    }

    // 2. 内部声明静态的类的对象,不进行初始化
    private static Order instance = null;

    // 3. 提供公共的静态方法,返回类的对象
    public static Order getInstance() {
        if (instance == null) {
            instance = new Order();
        }
        return instance;
    }
}

二者对比

  • 饿汉式:
    • 好处:线程安全。
    • 坏处:对象加载时间过长。
  • 懒汉式:
    • 好处:延迟对象的创建。
    • 坏处:线程不安全。

单例模式优点

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

单例模式应用场景

  • 网站的计数器,一般也是单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。
  • Application也是单例的典型应用。
  • Windows的TaskManager(任务管理器)就是很典型的单例模式。
  • Windows的RecycleBin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
final关键字
  • 修饰类:此类不能被其他类继承。
  • 修饰方法:此方法不能被重写,如Object类中的getClass()方法。
  • 修饰属性:此时的变量就是常量,考虑赋值的位置有:显示初始化、代码块中赋值、构造器中初始化。
public class FinalTest {
    final int LEFT = 1;
    final int RIGHT;
    final int UP;

    {
        RIGHT = 2;
    }

    public FinalTest() {
        UP = 3;
    }
}
  • 修饰局部变量:尤其是使用final修饰形参时,表示形参是一个常量,可以在方法体内使用该形参,但不能进行重新赋值。
public class FinalTest {
    @Test
    public void TestFinal() {
        int num = 10;
        new FinalTest().show(num);  // Final num = 10
    }

    public void show(final int num) {
        // num ++;  // 编译报错
        System.out.println("Final num = " + num);
    }
}
  • static final用来修饰全局常量,例如Math.PI的定义如下:
public static final double PI = 3.14159265358979323846;
抽象类与抽象方法

抽象类

关键字abstract修饰类,此类为抽象类:

  1. 此类不能实例化。
  2. 此类中有一定的构造器,便于子类实例化。
  3. 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关操作。

抽象方法

关键字abstract修饰方法,则为抽象方法:

  1. 抽象方法只有方法的声明,没有方法体。
  2. 包含抽象方法的类,一定是一个抽象类,反之则不一定。
  3. 若子类重写了父类的所有抽象方法,则该子类可以实例化;如果没有,则此子类也需要用abstract声明为抽象类。
import org.junit.Test;

public class AbstractTest {
    @Test
    public void testAbstract() {
        new Child("m", 20).info();  // Child: name = m, age = 20
        new Child().info();  // Child: name = null, age = 0
    }
}

// 抽象类
abstract class Human {
    String name;
    int age;

    public Human() {
    }

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

    // 抽象方法
    public abstract void info();
}

class Child extends Human {
    public Child() {
        super();
    }

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

    @Override
    public void info() {
        System.out.println("Child: name = " + this.name + ", age = " + age);
    }
}

abstract关键字的使用

主要注意:

  1. abstract不能用来修饰:属性、构造器等结构。
  2. abstract不能用来修饰私有方法、静态方法、final的方法和类。

抽象类的匿名子类对象

public class AbstractTest {
    @Test
    public void testAbstract() {
        // 可以直接用抽象父类Human的构造器(上面代码中的Child不行)
        Human stu = new Human("M", 30) {
            @Override
            public void info() {
                System.out.println("Student: name = " + this.name + ", age = " + age);
            }
        };
        stu.info();  // Student: name = M, age = 30
    }
}
接口

在Java中,类和接口是并列的概念。

接口的概述

Java的类不支持多重继承,有了接口(interface),就能得到多重继承的效果。接口的意义在于:

  • 有时需要从几个类中抽取一些共同的行为特征,但他们有没有is-a的关系,例如:鼠标、键盘、手机都支持USB连接。
  • 接口定义的是一组规范,继承是一个“是不是”的问题,接口实现则是一个“能不能”的问题。
  • 接口的本质是契约、标准、规范,实现后必须要遵守。

接口的使用

注意事项

  • JDK7之前,只能定义全局常量和抽象方法:
    • 全局常量:public static finanl(书写时可以省略)。
    • 抽象方法:public abstract(书写时可以省略)。
  • JDK8中除了全局常量和抽象方法外,还能定义静态方法、默认方法。
  • 接口不能定义构造器,这意味着接口不能实例化。
import org.junit.Test;

public class InterfaceTest {
    @Test
    public void testInterface() {
        System.out.println(Flyable.MIN_SPEED);  // 1
    }
}

interface Flyable {
    // 全局常量
    public static final int MAX_SPEED = 7900;
    int MIN_SPEED = 1;  // 省略了 public static final

    // 抽象方法
    public abstract void fly();
    void stop();  // 省略了 public abstract
}

实现接口

接口通过让类实现(implements)的方式来使用。

  • 如果实现类覆盖了接口中的所有抽象方法,则此实现类就可以实例化。
  • 如果没有,则此类实现类仍为一个抽象类。
import org.junit.Test;

public class InterfaceTest {
    @Test
    public void testInterface() {
        new Plane().fly();  // Plane fly.
        new Plane().stop();  // Plane stop.
    }
}

class Plane implements Flyable {
    @Override
    public void fly() {
        System.out.println("Plane fly.");
    }

    @Override
    public void stop() {
        System.out.println("Plane stop.");
    }
}

多实现接口

Java通过实现多个接口弥补了类的单继承性的局限性。

interface Attackable {
    void attack();
}

class Bullet implements Flyable, Attackable {
    @Override
    public void fly() {
    }

    @Override
    public void stop() {
    }

    @Override
    public void attack() {
    }
}

接口继承接口

接口可以多继承接口:

interface AA {
    void methodA();
}

interface BB {
    void methodB();
}

interface CC extends AA, BB {
    void methodC();
}

class DD implements CC {
    @Override
    public void methodA() {
    }

    @Override
    public void methodB() {
    }

    @Override
    public void methodC() {
    }
}

JDK8中接口的新特性

JDK8中除了全局常量和抽象方法外,还能定义静态方法、默认方法。

静态方法

接口中定义的静态方法,只能通过接口来调用,代码参考下一小节。

默认方法

默认方法使用default关键字修饰,通过实现类对象来调用。JDK8 API中对CollectionListComparator等接口提供了丰富的默认方法。

  • 如果实现类重写了接口的默认方法, 调用时调用的是重写后的方法。
import org.junit.Test;

public class InterfaceTest2 {
    @Test
    public void testInterface2() {
        CompareA.info();  // CompareA interface.
        // SubClass.info():  // 编译报错
        // new SubClass().info();  // 编译报错

        new SubClass().method1();  // CompareA method1.
        new SubClass().method2();  // CompareA method2.  SubClass method2.
    }
}

interface CompareA {
    public static void info() {
        System.out.println("CompareA interface.");
    }

    public default void method1() {
        System.out.println("CompareA method1.");
    }

    public default void method2() {
        System.out.println("CompareA method2.");
    }
}

class SubClass implements CompareA {
    @Override
    public void method2() {
        CompareA.super.method2();  // 调用父类的方法
        System.out.println("SubClass method2.");
    }
}
  • 如果实现类(子类)继承的父类和实现的接口中声明了同名同参数的方法,在实现类(子类)没有重写此方法时,默认调用的是父类中同名同参数的方法。
  • 如果实现类实现了多个接口,而这多个接口定义了同名同参数的默认方法,当在实现类没有重写此方法时,会报接口冲突的错误,所以此时实现类就必须重写此方法。
  • 调用父类方法,参考上述代码。
内部类

在Java中,允许一个类A的定义位于另一个类B的内部,类A称为内部类,类B称为外部类。内部类可以分为:

  • 成员内部类:静态、非静态。
  • 局部内部类:方法内、代码块内、构造器内。

成员内部类

一方面,成员内部类作为外部类的成员:

  • 可以调用外部类的结构。
  • 可以被static修饰。
  • 可以被4中不同的权限修饰。

另一方面,成员内部类作为一个类:

  • 类内可以定义属性、方法。
  • 可以被final修饰,表示此类不能被继承。
  • 可以被abstract修饰。