类和实例(class &instance)
面向对象中类(class)的概念与现实中类相近,可以理解为具有相应特征的集合,在Java中定义类的方法是class 类名 { }。例如,我们可以把大学理解为一个类,在Java中创建大学这个类: class University{ }。由此可知,类是一个相对抽象的概念,对于大学这个类来说,每一个所大学则是一个相对具体的存在,实例(instance)可以理解为一所具体的大学。在Java中创建一个实例龙大:University lgu = new University()。
在面向对象编程(OOP)中,我们习惯focus对象而非过程。例如我们想实现“龙大将要在2022年招收更多新生”。在OOP中,我们将创建两个类:龙大和新生。而非去创建一个名为招生的类。
字段(field)与方法(method)
一个类可以包含多个字段(field)来描述类的特征。
例如:
public class University{
private String name;
private int age; // code 1
private int rank;
}
一个大学类可以有多个字段描述其的特征,如校名,排名等。
一个类还可以有许多方法来实现该类的一些功能。
例如:
public class University{
public void admissions(){ // code 2
}
public void lowerScore(){
}
}
大学可以招生,压分等等。
封装
在创建类中字段时,如果我们public去修饰,那么我们可以在外部直接调用或更改字段,失去了对改字段更改的限制,有时会导致该字段失去意义。例如将大学排名从1000名改到-2名,显然不符合逻辑。所以我们习惯用private去修饰字段。这样我们就无法在外部更改或获取该字段。但我们仍然需要修改并获取字段的功能,我们将通过类的方法来实现,Java习惯用setter和getter分别表示设置和获取字段的方法。如下,我们就实现了name字段的封装。
public class University{
private String name;
private int age;
private int rank;
public void setName(String name){ // code 3
this.name = name;
}
public String getName(){
return this.name;
}
}
*此处用到this,可以理解为该类的对象。
构造方法
前文提到,当我们创建类的时候,我们需要setter方法来帮助我们设置字段的内容。然而当我们有许多字段的时候,我们将要多次调用不同的setter来设置字段,这是一项颇为重复的工作。用构造方法便可以解决这个问题。构造方法能在我们创建实例的同时设置多个字段。
University lgu = new University(); // code 4
lgu.setName("CUHKSZ"); // 未使用构造方法
University lgu = new University(name:"CUHKSZ",rank:38); //使用构造方法 code 5
在创建龙大实例的同时,我们同时传入了name和rank来完成初始化。
那么如何定义构造方法呢?在Java中,构造方法的名称就是类名。构造方法没用参数限制,并且可以在方法内部编写任何的语句。与普通方法相比,构造方法没用返回值,也没用void。例如大学类的构造方法:
public class University{
public University(String name, int rank){
this.name = name; // code 6
this.rank = rank;
}
}
我们就可以用该构造方法在创建lgu实例的同时设置两个字段的内容。
如何调用构造方法呢?前文已经举了例子,构造方法的调用必须用到new操作符:
University lgu = new University(name:"CUHKSZ",rank:38); // code 7
这样我们便可以在一行代码中就设置好字段。我们发现,调用构造方法的语句和我们创建实例时很像。实际上就是在调用构造方法,可我们之前并未定义构造方法,为什么可以调用new University?那是因为class拥有默认的构造方法。如果我们不去定义构造方法,编译器将会自动为我们去生成默认构造一个无参的构造方法。如下:
public class University{
public University(){ // code 8
}
}
所说每个类都有构造方法,而当我们自己去定义时,编译器就不再自动创建构造方法。当我们在自定义有参构造方法后仍用code 4 中的代码去创建实例时,将会报错。所以在我们习惯在自定义有参构造方法时同时再去定义一个无参的构造方法(这还有很多作用,后面会提到)。
此外,没用构造方法初始化字段时,String的默认是null,int是0,Boolean是false。
我们在字段中也可以进行初始化:
public class University{
private String name = "野鸡大学";
private int rank = 1000; // code 9
public University(String name,int rank){
this.name = name;
this.rank= rank;
}
}
这时,假如我们执行code 7中的代码。name字段为“CUHKSZ”,rank为38,而不是野鸡大学和1000。其实这里的执行顺序是这样的:先执行初始化字段,然后再执行构造方法初始化。
当有多种构造方法时,Java将根据构造方法的参数、位置和类型来自动匹配。而在构造方法中调用其他构造方法,则需要用到this(…):
public class University{
private String name;
private int rank; // code 10
public University(String name,int rank){
this.name = name;
this.rank= rank;
}
public University(){
this("野鸡大学",1000)
}
}
方法重载(Overload)
类中定义方法名相同但是参数不同的方法的操作称为方法重载(Overload),这种操作便于我们后续对类的使用。方法重载的相互调用用到this(…),注意调用的一些细节,如下代码将会报错,错因就是标红的代码this()。这是相互调用导致的错误。
public class University{
private String name;
private int rank; // code 11
public University(String name,int rank){
this();
this.name = name;
this.rank= rank;
}
public University(){
this("野鸡大学",1000)
}
}
继承
继承的意义和实现
面向对象中的继承能够有效避免许多重复的代码。当我们想创建一个双非类,双非类拥有大学类的所有特征,为了避免重复写代码,我们可以让双非类继承大学类。此处大学类被称为父类,也叫做基类,超类;子类也叫做派生类。我们让双非类继承大学类:
public class DoubleNon extends University{ // code 11
}
那么双非就有了大学类的所有功能。
Java一次只能继承一个父类,不支持多继承,这点和python不同。当没有写extend时,编译器会自动继承Object类,这点和python类似。
子类访问父类的问题
子类无法访问父类的private字段和方法,但不代表子类没有继承父类的private字段/方法。实际上,在同包下,子类将继承父类所有的字段和方法,只是在private下,直接访问受到限制。而如果改用protected,则可以在子类当中直接访问。
构造方法的继承问题
子类的构造方法中一定要调用一次父类的构造方法。如何理解这句话,我们可以从三种情况去考虑。
情况一:父类没有自定义有参构造方法,那么编译器默认定义无参的构造方法,故在子类中,无论子类是否自定义了构造方法,都会在构造方法中默认调用父类的构造方法。
情况二:父类自定义了有参构造方法,但同时自定义了无参构造方法。编译器同样会在子类中的构造方法中去默认调用。
情况三:父类自定义了有参构造方法,并未定义无参构造方法,由于我们定义了构造方法,编译器不会帮我们创建构造方法,所以我们在继承时会报错,此时需要在子类的构造方法中调用父类的构造方法。而且必须在第一行调用。形式是:
super("…"); // code 12
*此处涉及到了super(),super可以理解为父类的对象。可以通过super.字段名调用父类字段。
同时,我们可以发现子类无法继承父类的构造方法。
向上转型(upcasting)&向下转型(down casting)
此处,我们考虑一种创建实例的情况:
University lgu = new DoubleNon(); // code 13
这种写法并不会报错,lgu的引用变量是University,是指向DoubleNon的实例。DoubleNon是University的子类,它继承了University的所有字段和方法,University类的变量指向DoubleNon类的实例是没问题的。这种把子类变为父类的赋值就是向上转型。可以理解为将一个对象更加抽象化。比如说lgu是双非,但我们将其转为大学类的变量。但它仍然指向双非类的实例。它拥有双非类的所有特征,却将自己包装成大学。向上转型其实在实际操作中很常用。假如现在有个方法叫做评选双一流。而所有大学都可以参与双一流大学的评选,所以这个方法的参数是大学类。但在我们创建各个大学的实例时,我们可能用大学的子类将各个实例化。那么评选双一流这个方法就似乎需要接受很多参数,但由于向上转型,并不会有这个问题存在。
与之相对应的就是向下转型(Downcasting)。实现的语句如下:
University lgu = new DoubleNon;
DoubleNon cusz = (DoubleNon)lgu;
大学类变量lgu实际指向双非实例,通过向下转型,就有了指向双非实例的双非类变量cusz。
多态
方法重写(Override)
子类可以通过方法重写去修改或重新定义父类中的方法。与方法重载不同,方法重写要求方法签名和返回值都相同。
public class University{
public void admission(){
system.out.println("University is recruiting.");
}
}
public class C9 extends University{
public void admission(){
system.out.println("C9 league is recruiting.");
}
}
当我们创建一个实例:University tju = new C9();时tju的引用类型是University,实际类型是C9。我们调用tju的招生方法时:tju.admission; 输出将是:
C9 is recruiting. 说明在调用方法时,所调用的并不是所声明的应用类型,而是其运行中的实际实际类型,这便是多态。
抽象类
多态可以让所有的子类都重写父类的方法,假如大学类的子类都重写了大学的招生方法,在大学类里面调用招生方法似乎就没有意义了。如果我们删掉方法的执行语句(“{}”及其内容),显然是不行的,但如果直接删除招生的方法。考虑下面的方法将会报错。
Public void Admission(University u){
u.admission;
}
如果只为了定义方法的签名而让子类去重写,可以把父类的方法声明为抽象方法。此时父类也需要定义为抽象类。
abstract class University{
public abstract void admission();
}
抽象类设计主要是为了让子类去继承并重写抽象方法,因为抽象类本身无法被实例化:
University lgu = new University; 将会报错。但我们通过抽象类University去引用其他的子类实例:University tju = C9();我们调用其方法时就不用在去考虑University类型变量的具体子类类型是什么。
接口
什么是接口?在Java中,我们如下定义:
interface University{
void admission();
}
在接口中,所有方法默认是抽象的,故不用加上public abstract。
类实现接口用到implements关键字:
Class C9 implements University{
Public void admission(){
System.out.printlin("C9 is recruiting.");
}
}
Java中一个类只能继承一个类,但可以继承多个接口这也是设计接口的一个原因。
接口的字段只能是public static final类型。