类与对象

类就是对一类事物的描述,是抽象的定义,对象是某一类实际存在的每个个体,也被称为实例。

类的定义与对象创建

Main也是一个类,只不过是主类,也就是编写主方法的类。

如果我们想创建一个“人”类,则新建Person类,命名遵循首字母大写无特殊字符。

1
2
3
4
package org.ep;

public class Person {
}

创建好一个类后,我们需要给类添加一些属性,直接作为类的成员变量定义到类中

1
2
3
4
5
6
7
package org.ep;

public class Person {
String name;
int age;
String sex;
}

添加成员变量后就可以创建一个实例对象了

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
new Person();
}
}

对象的使用

如果我们想访问一个对象,即知道我们创建的这个实例对象这个人的姓名、年龄、性别,就需要一个变量来指代这个对象,只不过引用类型的变量,存储的是对象的引用,而不是对象本身:

1
2
3
4
5
6
7
8
9
10
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person(); //创建一个变量指代我们创建的对象
Person p1 = p; //将p赋值给p1,实际上只是传递了对象的引用,类似于p和p1是指向同一个对象的指针

System.out.println(p == p1); //验证
}
}

输出

1
true

证明p和p1两个变量引用的是同一个对象。

有了对象的引用之后,就可以对对象进行操作了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.name = "ep";
p.age = 22;
p.sex = "Male";

System.out.println(p.name);
System.out.println(p.age);
System.out.println(p.sex);
}
}

输出

1
2
3
ep
22
Male

我们也可以不引用任何对象,即

1
Person p = null;

如果直接创建对象而不给其属性赋值,那么对象的属性都会存在初始值,如果是基本类型,那么默认是统一为0(如果是boolean的话,默认值为false)如果是引用类型,那么默认是null

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();

System.out.println("p.name = " + p.name);
System.out.println("p.age = " + p.age);
System.out.println("p.sex = " + p.sex);
}
}

输出

1
2
3
p.name = null
p.age = 0
p.sex = null

方法

方法的创建与使用

有了属性就可以为创建的这些对象设定不同的属性值,比如每个人的名字都不一样,性别不一样,年龄不一样等。我们可以定义方法来实现(即函数)。

方法的定义如下:

1
2
3
返回值类型 方法名称() {
方法体...
}

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

public class Person {
String name;
int age;
String sex;

void greet() {
System.out.println("hello! my name is "+ name);
}

int add(int a, int b) {
return a + b;
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.name = "himmel";

p.greet();
System.out.println(p.add(3, 4));
}
}

输出

1
2
hello! my name is himmel
7

有时我们的方法中可能会出现与成员变量重名的变量,假如我们想使用setName()给实例设定名字即更改成员变量p.name,而我们在方法setName(String name)中的变量name与其重名

1
2
3
void setName(String name) {

}

我们就可以使用this关键字来明确规定this.name代表当前类的实例的成员变量,使用例:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Person {
String name;
int age;
String sex;

void setName(String name) {
this.name = name;
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.name = "himmel";

System.out.println(p.name);

p.setName("himmelszelt");
System.out.println(p.name);
}
}

输出

1
2
himmel
himmelszelt

方法的重载

例如上面的add方法,参数要求的是int类型,但如果我们同时想用一个方式既能实现整数运算也能实现浮点数运算,就可以用到方法的重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.ep;

public class Person {
String name;
int age;
String sex;

int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}
}

调用

1
2
3
4
5
6
7
8
9
10
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();

System.out.println(p.add(3, 4));
System.out.println(p.add(3.3, 4.4));
}
}

输出

1
2
7
7.7

构造方法

前面创建对象,直接使用new就可以了,但是各种属性都是默认值,使用构造方法就可以为对象的属性赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Person {
String name;
int age;
String sex;

Person() {
name = "himmel";
age = 22;
sex = "male";
}
}

构造方法会在new创建对象的时候自动执行

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person();

System.out.println(p.name);
System.out.println(p.age);
System.out.println(p.sex);
}
}

输出

1
2
3
himmel
22
male

也可以为构造方法设定参数

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.ep;

public class Person {
String name;
int age;
String sex;

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

然后在new的时候输入参数,就可以直接将实例中的属性初始化:

1
Person p = new Person("himmel", 22, "Male");

静态变量和静态方法

一个类可以具有多种属性、行为,包括对象该如何创建,我们可以通过构造方法进行设定,我们可以通过类创建对象,每个对象都会具有我们在类中设定好的属性,包括我们设定好的行为,所以说类就像是一个模板,我们可以通过这个模板快速捏造出一个又一个的对象。

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

1
2
3
4
5
6
7
8
9
package org.ep;

public class Person {
String name;
int age;
String sex;

static String school;
}

调用

1
2
3
4
5
6
7
8
9
10
package org.ep;

public class Main {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
p1.school = "see";
System.out.println(p2.school);
}
}

输出

1
see

在这里我们设定了属于对象p1的静态属性的值,而p2的同一个属性也随之设定。所以一般情况下我们不会通过一个具体的对象去修改和使用静态属性,而是通过整个类去使用

1
Person.school = "see";

同样的,我们可以将方法标记为静态

1
2
3
static void test(){
System.out.println("静态方法");
}

静态方法同样是属于类的,而不是具体的某个对象。

因为静态方法是属于类的,所以在静态方法中无法获取成员变量的值。
intro1
intro2
同样的,在静态方法中,无法使用this关键字,因为this关键字代表的是当前的对象本身。

但是静态方法是可以访问静态变量:

1
2
3
4
5
static String school;

static void intr() {
System.out.println("my school is " + school);
}

静态变量的初始化

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person {
String name = test(); //这里我们用test方法的返回值作为变量的初始值,便于观察
int age;
String sex;

{
System.out.println("我是普通代码块");
}

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

String test(){
System.out.println("我是成员变量初始化");
return "小明";
}

static String info = init(); //这里我们用init静态方法的返回值作为变量的初始值,便于观察

static {
System.out.println("我是静态代码块");
}

static String init(){
System.out.println("我是静态变量初始化");
return "test";
}
}

输出

1
2
3
4
5
我是静态变量初始化
我是静态代码块
我是成员变量初始化
我是普通代码块
我是构造方法

可以看到,确实是静态内容在对象构造之前的就完成了初始化,实际上就是类初始化时完成的。

包的访问与控制

包声明和导入

包其实就是用来区分类位置的东西,也可以用来将我们的类进行分类。随着我们的程序不断变大,可能会创建各种各样的类,他们可能会做不同的事情,那么这些类如果都放在一起的话,有点混乱,我们可以通过包的形式将这些类进行分类存放。

package用于指定当前类所处的包的,所处的包和对应的目录是一一对应的。

当我们使用同一个包中的类时,直接使用即可。而当我们需要使用其他包中的类时,需要先进行导入才可以:import。只有在类不在同一个包下时才需要进行导入,如果一个包中有多个类,我们可以使用*表示导入这个包中全部的类:

1
import com.test.entity.*;

一直在使用的System类,也是在一个包中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package java.lang;

import java.io.*;
import java.lang.reflect.Executable;
import java.lang.annotation.Annotation;
import java.security.AccessControlContext;
import java.util.Properties;
import java.util.PropertyPermission;
import java.util.StringTokenizer;
import java.util.Map;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.AllPermission;
import java.nio.channels.Channel;
import java.nio.channels.spi.SelectorProvider;
import sun.nio.ch.Interruptible;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
import sun.security.util.SecurityConstants;
import sun.reflect.annotation.AnnotationType;

import jdk.internal.util.StaticProperty;

/**
* The <code>System</code> class contains several useful class fields
* and methods. It cannot be instantiated.
*
* <p>Among the facilities provided by the <code>System</code> class
* are standard input, standard output, and error output streams;
* access to externally defined properties and environment
* variables; a means of loading files and libraries; and a utility
* method for quickly copying a portion of an array.
*
* @author unascribed
* @since JDK1.0
*/
public final class System {
...
}

它是属于java.lang这个包下的类,并且这个类也导入了很多其他包中的类在进行使用。而实际上Java中会默认导入java.lang这个包下的所有类,因此我们不需要手动指定。

访问权限控制

Java中引入了访问权限控制(可见性),我们可以为成员变量、成员方法、静态变量、静态方法甚至是类指定访问权限,不同的访问权限,有着不同程度的访问限制:

  • private:私有,标记为私有的内容无法被除当前类以外的任何位置访问。
  • 什么都不写:默认,默认情况下,只能被类本身和同包中的其他类访问。
  • protected:受保护,标记为受保护的内容可以能被类本身和同包中的其他类访问,也可以被子类访问(子类我们会在下一章介绍)
  • public:公共,标记为公共的内容,允许在任何地方被访问。

继承、封装和多态

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

封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。

继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

类的封装

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

对之前的类进行改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.ep;

public class Person {
private String name;
private int age;
private String sex;

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

public String getName() {
return name;
}

public int getAge() {
return age;
}

public String getSex() {
return sex;
}
}

因为将nameagesex私有化,所以不能直接获取成员属性,只能通过Person类提供的方法来获取成员属性

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Person("E", 22, "Male");

System.out.println(p.getName());
System.out.println(p.getAge());
System.out.println(p.getSex());
}
}

输出

1
2
3
E
22
Male

也可以在Person类提供的方法中进行一些操作,比如修改名字但不能包含特定字符

1
2
3
4
5
public void setName(String name) {
if (name.contains("."))
return;
this.name = name;
}

还可以将构造方法改成私有的,需要通过我们的内部的方式来构造对象:

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private String name;
private int age;
private String sex;

private Person(){} //不允许外部使用new关键字创建对象

public static Person getInstance() { //而是需要使用我们的独特方法来生成对象并返回
return new Person();
}
}

通过这种方式可以实现单例模式(后续Java设计模式)

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。

类的继承

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

比如说我们一开始使用的人类,那么实际上人类根据职业划分,所掌握的技能也会不同,可以将人类这个大类根据职业进一步地细分出来:

创建一个父类Person

1
2
3
4
5
6
7
package org.ep;

public class Person {
String name;
int age;
String sex;
}

接着可以创建各种各样的子类,继承父类需要用到关键字extends:

1
2
3
4
5
package org.ep;

public class Student extends Person{

}
1
2
3
4
5
package org.ep;

public class Worker extends Person {

}

类的继承可以不断向下,但是同时只能继承一个类,且标记为final的类不允许被继承

1
2
3
4
5
6
7
package org.ep;

public final class Person { //不可被继承
String name;
int age;
String sex;
}

当一个类继承另一个类时,属性会被继承,可以直接访问父类中定义的属性,除非父类中将属性的访问权限修改为private,那么子类将无法访问(但是依然是继承了这个属性的):

1
2
3
4
5
6
7
package org.ep;

public class Student extends Person{
public void Study() {
System.out.println("my name is " + name +", im studying");
}
}

在父类中定义的方法会被子类继承

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Person {
String name;
int age;
String sex;

public void greet() {
System.out.println("my name is " + name + ", hello");
}
}

子类直接继承方法,在创建子类对象时可以直接使用:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Student stu = new Student();
stu.name = "E";
stu.greet();
}
}

输出

1
my name is E, hello

如果父类存在一个有参构造方法,子类必须在构造方法中调用。即不仅要初始化子类的属性,还需要初始化父类的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//父类
package org.ep;

public class Person {
protected String name; //子类需要继承这些属性,同时不想让外部修改,所以设定为protected
protected int age;
protected String sex;
protected String profession;

protected Person(String name, int age, String sex, String profession) {
this.name = name;
this.age = age;
this.sex = sex;
this.profession = profession;
}

public void greet() {
System.out.println("my name is " + name + ", hello");
}
}

子类在构造时,不仅要初始化子类的属性,还需要初始化父类的属性,所以说在默认情况下,子类其实是调用了父类的构造方法的,只是在无参的情况下可以省略,但是现在父类构造方法需要参数,那么我们就需要手动指定了:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Student extends Person{
public Student(String name, int age, String sex) { //职业已经确定,直接在初始化父类属性时声明即可
super(name, age, sex, "Studeng"); //super代表父类
}

public void study() {
System.out.println("my name is " + name + ", im studying");
}
}
1
2
3
4
5
6
7
package org.ep;

public class Worker extends Person {
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}
}

在使用子类时,可以将其当做父类来使用:

1
2
3
4
5
6
7
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Student("E", 22, "Male");
}
}

我们也可以使用强制类型转换,将一个被当做父类使用的子类对象,转换回子类:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Student("E", 22, "Male");
Student student = (Student) p;
student.study();
}
}

输出

1
my name is E, im studying

但是注意,这种方式只适用于这个对象本身就是对应的子类才可以,如果本身都不是这个子类,或者说就是父类,那么会出现问题,比如无法将一个Worker子类的变量转换为Student子类的变量。

想知道某个变量引用的对象是什么类,使用instanceof

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Person p = new Student("E", 22, "Male");
if (p instanceof Student)
System.out.println("Student");
if (p instanceof Person)
System.out.println("Person");
}
}

输出

1
2
Student
Person

也就是说,如果对象是一个子类,那么判断其父类也会返回true

子类可以和父类定义同名属性:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Worker extends Person {
protected String name;
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}
}

此时父类Person和子类Worker的属性name同时存在,而我们在子类中直接使用时会被当做作用域最近的一个,也就是在当前子类中定义的name属性,而不是父类的name属性。

1
2
3
4
5
6
7
8
9
10
11
12
package org.ep;

public class Worker extends Person {
protected String name;
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}

public void work() {
System.out.println("my name is " + name + ", im working");
}
}

输出

1
my name is null, im working

此时可以用super访问父类的同名属性:

1
2
3
4
5
6
7
8
9
10
11
12
package org.ep;

public class Worker extends Person {
protected String name;
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}

public void work() {
System.out.println("my name is " + super.name + ", im working");
}
}

输出

1
my name is E, im working

需要注意的是,没有super.super这种用法,也就是说如果存在多级继承的话,那么最多只能通过这种方法访问到一级父类的属性。

顶层Object

实际上所有类都默认继承自Object类,除非手动指定继承的类型,但是依然改变不了最顶层的父类是Object类。所有类都包含Object类中的方法,Object类的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Object {

private static native void registerNatives(); //标记为native的方法是本地方法,底层是由C++实现的
static {
registerNatives(); //这个类在初始化时会对类中其他本地方法进行注册,本地方法不是我们SE中需要学习的内容,我们会在JVM篇视频教程中进行介绍
}

//获取当前的类型Class对象,这个我们会在最后一章的反射中进行讲解,目前暂时不会用到
public final native Class<?> getClass();

//获取对象的哈希值,我们会在第五章集合类中使用到,目前各位小伙伴就暂时理解为会返回对象存放的内存地址
public native int hashCode();

//判断当前对象和给定对象是否相等,默认实现是直接用等号判断,也就是直接判断是否为同一个对象
public boolean equals(Object obj) {
return (this == obj);
}

//克隆当前对象,可以将复制一个完全一样的对象出来,包括对象的各个属性
protected native Object clone() throws CloneNotSupportedException;

//将当前对象转换为String的形式,默认情况下格式为 完整类名@十六进制哈希值
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

//唤醒一个等待当前对象锁的线程,有关锁的内容,我们会在第六章多线程部分中讲解,目前暂时不会用到
public final native void notify();

//唤醒所有等待当前对象锁的线程,同上
public final native void notifyAll();

//使得持有当前对象锁的线程进入等待状态,同上
public final native void wait(long timeout) throws InterruptedException;

//同上
public final void wait(long timeout, int nanos) throws InterruptedException {
...
}

//同上
public final void wait() throws InterruptedException {
...
}

//当对象被判定为已经不再使用的“垃圾”时,在回收之前,会由JVM来调用一次此方法进行资源释放之类的操作,这同样不是SE中需要学习的内容,这个方法我们会在JVM篇视频教程中详细介绍,目前暂时不会用到
protected void finalize() throws Throwable { }
}

尝试调用Object提供的toString()方法:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Worker worker = new Worker("E", 22, "Male");
String str = worker.toString();
System.out.println(str);
}
}
1
org.ep.Worker@1b6d3586

默认提供的equals方法:

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Person p1 = new Student("E", 22, "Male");
Person p2 = new Student("E", 22, "Male");
System.out.println(p1.equals(p2));
}
}
1
false

可以发现,虽然实例p1和p2的属性都相同,但是得到的结果是false,是因为equals()比较的是二者是否是同一对象,那如果我们想要比较的双方属性一样就返回true,就需要修改一下默认的equals()方法,要用到方法的重写。

方法的重写

不同于先前的方法的重载,重载是为某个方法提供更多种类,重写是覆盖原有的方法实现。比如我们不希望使用Object类中默认的equals()方法,就可以把他重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.ep;

public class Person {
protected String name; //子类需要继承这些属性,同时不想让外部修改,所以设定为protected
protected int age;
protected String sex;
protected String profession;

protected Person(String name, int age, String sex, String profession) {
this.name = name;
this.age = age;
this.sex = sex;
this.profession = profession;
}

public boolean equals(Object obj) { //重写的方法必须与父类的定义完全一致
if (obj == null)
return false; //如果传入对象为null肯定不相等
if (obj instanceof Person) { //只有传入对象是当前对象或其的子类才能对比
Person person = (Person) obj; //转化为同种类型,然后对所有属性依次比较
return this.name.equals(person.name) && this.age == person.age && this.sex.equals(person.sex);
}
return false;
}
}

重写后就可以按照我们的想法进行判断了

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Person p1 = new Student("E", 22, "Male");
Person p2 = new Student("E", 22, "Male");
System.out.println(p1.equals(p2));
}
}
1
true

类似的,为了得到不同对象的各个属性,我们可以重写toString()方法

1
2
3
public String toString() {
return "name = '" + name +'\'' + ", age = '" + age + '\'' + ", sex = '" + sex + '\'' + ", profession = '" + profession + '\'';
}

就可以直接输出对象的属性了

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) {
Person p1 = new Student("E", 22, "Male");
Person p2 = new Student("E", 22, "Male");
System.out.println(p1.toString());
}
}
1
name = 'E', age = '22', sex = 'Male', profession = 'Student'

需要注意的是,静态方法不支持重写,因为它是属于类本身的,但是它可以被继承。

基于这种方法可以重写的特性,对于一个类定义的行为,不同的子类可以出现不同的行为,比如考试,学生考试可以得到A,而工人去考试只能得到D:

1
2
3
4
//Person.java
public void exam() {
System.out.println("exam");
}
1
2
3
4
//Student.java
public void exam() {
System.out.println("my name is "+ name +", i got an A");
}
1
2
3
4
//Worker.java
public void exam() {
System.out.println("my name is "+ name +", i got a D");
}

调用exam(),对于同一个方法会产生不同的结果

1
2
3
4
5
6
7
8
9
10
package org.ep;

public class Main {
public static void main(String[] args) {
Person p1 = new Student("E", 22, "Male");
Person p2 = new Worker("P", 22, "Male");
p1.exam();;
p2.exam();
}
}
1
2
my name is E, i got an A
my name is P, i got a D

如果在父类中不想让子类重写某个方法,可以在方法前添加final关键字:

1
2
3
public final void exam() {
System.out.println("exam");
}

或者,如果父类中方法的可见性为private,那么子类同样无法访问,也就不能重写,但是可以定义同名方法,此时虽然能编译通过,但是并不是对父类方法的重写,仅仅是子类自己创建的一个新方法。

在重写父类方法时,如果希望调用父类原本的方法实现,那么同样可以使用super关键字:

1
2
3
4
5
//Student.java
public void exam() {
super.exam();
System.out.println("my name is "+ name +", i got an A");
}
1
2
exam
my name is E, i got an A

子类在重写父类方法时,不能降低父类方法中的可见性,比如父类的exampublic,子类的examprotected。因为子类实际上可以当做父类使用,如果子类的访问权限比父类还低,那么在被当做父类使用时,就可能出现无视访问权限调用的情况,但是相反的,我们可以在子类中提升权限

抽象类

在有了类的重写与继承之后,越处于顶层定义的类,实际上可以进一步地进行抽象,比如我们前面编写的exam()方法,这个方法再子类中一定会被重写,所以说除非子类中调用父类的实现,否则一般情况下永远都不会被调用,所以说,我们可以将Person类进行进一步的抽象,让某些方法完全由子类来实现,父类中不需要提供实现。

要实现这样的操作,我们可以将Person变成抽象类,抽象类比类还要抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.ep;

public abstract class Person { //通过添加abstract关键字,表示这个类是一个抽象类
protected String name;
protected int age;
protected String sex;
protected String profession;

protected Person(String name, int age, String sex, String profession) {
this.name = name;
this.age = age;
this.sex = sex;
this.profession = profession;
}

public abstract void exam() { //抽象类中可以具有抽象方法,也就是说这个方法只有定义,没有方法体
}
}

而具体的实现,需要由子类来完成,而且如果是子类,必须要实现抽象类中所有抽象方法:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Worker extends Person {
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}

public void exam() { //子类必须要实现抽象类所有的抽象方法,这是强制要求的,否则会无法通过编译
System.out.println("my name is "+ name +", i got a D");
}
}

抽象类由于不是具体的类定义(它是类的抽象)可能会存在某些方法没有实现,因此无法直接通过new关键字来直接创建对象,要使用抽象类,我们只能去创建它的子类对象。

抽象类一般只用作继承使用,当然,抽象类的子类也可以是一个抽象类:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public abstract class Worker extends Person {
public Worker(String name, int age, String sex) {
super(name, age, sex, "Worker");
}

public void exam() { //抽象类中并不是只能有抽象方法,抽象类中也可以有正常方法的实现
System.out.println("my name is "+ name +", i got a D");
}
}

如果抽象类的子类也是抽象类,那么可以不用实现父类中的抽象方法。

需要注意的是,抽象方法的访问权限不能为private,因为抽象方法一定要由子类实现。

接口

接口只表示某个确切的功能,只包含方法的定义。接口一般只代表某些功能的抽象,接口包含了一些类方法的定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)。

通俗来说,就是将类所具有某些的行为抽象出来。比如,对于人类的不同子类,学生和老师来说,他们都具有学习这个能力,既然都有,那么我们就可以将学习这个能力,抽象成接口来进行使用,只要是实现这个接口的类,都有学习的能力:

1
2
3
4
5
package org.ep;

public interface Study {
void study(); //接口中只能定义访问权限为public抽象方法,其中public和abstract关键字可以省略
}

StudentTeacher子类使用implement关键字实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Student extends Person implements Study {
public Student(String name, int age, String sex) {
super(name, age, sex, "Student");
}

public void study() {
System.out.println("my name is " + name + ", im studying");
}
}
1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Teacher extends Person implements Study{
public Teacher(String name, int age, String sex) {
super(name, age, sex, "Teacher");
}

public void study() {
System.out.println("my name is " + name + ", im teaching");
}
}

接口不同于类的继承,接口可以同时实现多个,同时实现多个接口时,在implements关键字后用逗号分隔。

接口像是一个类的功能列表,作为附加功能存在,一个类可以附加很多个功能。可以说是继承的一种替代方案。

接口和抽象类类似,不能直接创建对象,但是我们也可以将接口实现类的对象以接口的形式去使用:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) {
Study study = new Teacher("C", 30, "Female");
if (study instanceof Teacher) {
Teacher teacher = (Teacher) study;
teacher.study();
}
}
}
1
my name is C, im teaching

这里的使用其实跟之前的父类是差不多的。

接口中可以存在方法的默认实现:

1
2
3
4
5
6
7
8
9
package org.ep;

public interface Study {
void study();

default void test() {
System.out.println("default");
}
}

如果方法在接口中存在默认实现,那么实现类中不强制要求进行实现。

接口不同于类,接口中不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法:

1
2
3
4
5
6
7
8
9
10
package org.ep;

public interface Study {
public static final int a = 10;
public static void test(){
System.out.println("静态方法");
};

void study();
}

跟普通的类一样,我们可以直接通过接口名.的方式使用静态内容:

1
2
3
4
5
6
7
8
package org.ep;

public class Main {
public static void main(String[] args) {
System.out.println(Study.a);
Study.test();
}
}
1
2
10
静态方法

接口是可以继承自其他接口的,并且接口没有继承数量限制,接口支持多继承,本质上是对多个接口功能的融合:

1
2
3
public interface A exetnds B, C, D {

}

如果想复制一个属性完全相同的对象,就需要使用Object类中提供的克隆方法:

1
2
3
4
package org.ep;

public interface Cloneable { //这个接口中什么都没定义
}

实现接口后,我们还需要将克隆方法的可见性提升一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.ep;

public class Student extends Person implements Study, java.lang.Cloneable { //首先实现Cloneable接口,表示这个类具有克隆的功能
public Student(String name, int age, String sex) {
super(name, age, sex, "Student");
}

public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
return super.clone(); //因为底层是C++实现,我们直接调用父类的实现就可以了
}

public String toString() {
return "name = '" + name +'\'' + ", age = '" + age + '\'' + ", sex = '" + sex + '\'' + ", profession = '" + profession + '\'';
}

public void study() {
System.out.println("my name is " + name + ", im studying");
}
}

实现:

1
2
3
4
5
6
7
8
9
10
11
package org.ep;

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("E", 22, "Male");
Student clone = (Student) student.clone();
System.out.println(student);
System.out.println(clone);
System.out.println(student == clone);
}
}
1
2
3
name = 'E', age = '22', sex = 'Male', profession = 'Student'
name = 'E', age = '22', sex = 'Male', profession = 'Student'
false

我们得到了两个属性完全相同的对象,且判断了不是同一个对象。

枚举类

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.ep;

public class Student extends Person implements Study {
public Student(String name, int age, String sex) {
super(name, age, sex, "Student");
}

public String toString() {
return "name = '" + name +'\'' + ", age = '" + age + '\'' + ", sex = '" + sex + '\'' + ", profession = '" + profession + '\'';
}

public void study() {
System.out.println("my name is " + name + ", im studying");
}

private String status; //状态,可以是跑步、学习、睡觉这三个之中的其中一种

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}
}

但可能会出现一些问题,我们希望status只包含跑步、学习、睡觉三个之一,但外界有可能会输入其他的字符串,此时我们需要枚举类enum来实现此功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.ep;

public class Student extends Person implements Study {
public Student(String name, int age, String sex) {
super(name, age, sex, "Student");
}

public String toString() {
return "name = '" + name +'\'' + ", age = '" + age + '\'' + ", sex = '" + sex + '\'' + ", profession = '" + profession + '\'';
}

public void study() {
System.out.println("my name is " + name + ", im studying");
}

public enum Status {
RUNNING, STUDY, SLEEP;
}

private Status status;

public Status getStatus() {
return status;
}

public void setStatus(Status status) {
this.status = status;
}
}

这样在使用时,就能提示我们使用的方法支持哪些了:
枚举类

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

我们也可以给枚举类型添加独有的成员方法:

1
2
3
4
5
6
7
8
9
10
11
12
public enum Status {
RUNNING("跑步"), STUDY("学习"), SLEEP("睡觉"); //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法)

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

public String getName() { //获取封装的成员变量
return name;
}
}

这样枚举类就能按照我们想要的中文名称打印了,即打印出枚举的参数

1
2
3
4
5
6
7
8
9
package org.ep;

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("E", 22, "Male");
student.setStatus(Student.Status.RUNNING);
System.out.println(student.getStatus().getName());
}
}
1
跑步