本博客将介绍Java面向对象的一些核心概念,譬如:构造函数(constructor),继承(inheritance,is-a),this关键字,super关键字,重载(overload),重写(override),静态属性与方法(static variables & methods),实例属性(field)与实例方法,组合(composition,has-a),封装(encapsulation),多态(polymorphism)。当然,这篇博客实际上也是我在学习udemy上Java Programming Masterclass for Software Developers这门课程的学习笔记。By the way,Tim Buchalka老师讲的真的非常好:)
1. 构造函数
Java类定义中构造函数应该是比较重要的一个概念,构造函数有以下要求,方法名必须与类名一致,形参列表和返回值的类型倒没有什么限制。
构造函数的具体作用是类初始化实例对象时调用,用于给一些实例属性赋值。它与setter的功能实际上是有些重复的,例如:
public class Animal { private String name; private int brain; private int body; private int size; private int weight; public Animal(String name, int brain, int body, int size, int weight) { this.name = name; this.brain = brain; this.body = body; this.size = size; this.weight = weight; } public String getName() { return name; } public int getBrain() { return brain; } public int getBody() { return body; } public int getSize() { return size; } public int getWeight() { return weight; } }
也可以写成如下的setter的形式:
public class Animal { private String name; private int brain; private int body; private int size; private int weight; //Tips:在IntelliJ IDEA中,生成私有属性对应的setter,getter以及constructor方法是非常简单的 //Code => Generate => 选择需要生成的对应方法即可,还可以指定对应的实例属性 public void setName(String name) { this.name = name; } public void setBrain(int brain) { this.brain = brain; } public void setBody(int body) { this.body = body; } public void setSize(int size) { this.size = size; } public void setWeight(int weight) { this.weight = weight; } public String getName() { return name; } public int getBrain() { return brain; } public int getBody() { return body; } public int getSize() { return size; } public int getWeight() { return weight; } }
setter和constructor中都可以对初始化的值进行一些操作,比如校验初始化值的大小范围等等。(至于为何必须将field属性写成私有属性,同时通过setter和getter暴露出去,则是基于封装的特性,封装的优点将在后面介绍)。
实际上编码中,考虑到setter方法的繁琐,以及基于构造函数还能够重载(overload)的特性,所以往往都是建议采用构造函数的形式,这样能够取代setter繁琐的初始化实例变量的过程。(此外,如果类中未定义构造函数,那么Java编译器会自动添加一个无形参列表的构造函数)
接下来说一下,构造函数的一些其他特性(优点):
1. 构造函数可以重载(overload)
简单介绍一下,重载方法的特点是:方法名必须相同,形参列表必须不同,返回值的类型没有限制。
实际中,重载通常会出现在以下场景中:
(1)构造函数的重载;
(2)子类继承父类,子类中存在同名方法,重载了父类中的方法;
那么,构造函数的重载有什么意义呢?不同形参列表的构造函数,可以让初始化对象实例的过程中,传入的参数能够更加的灵活。编译器会根据传入的参数不同,而去调用不同的构造函数,完成初始化的过程。而且,结合this(),能够写出比较优雅的构造函数链(constructor chaining)。
2. 构造函数可以互相调用
构造函数本质上也是方法,同一个类中的方法间当然是可以互相调用的(除了静态方法),这样也就形成了constructor chaining的概念。例如:
package com.zhoujh; public class Account { private String accountNmuber; private double balance; private String name; private String email; private String phone; public Account(String accountNmuber, double balance, String name, String email, String phone) { this.accountNmuber = accountNmuber; this.balance = balance; this.name = name; this.email = email; this.phone = phone; } public Account(String name, String email, String phone) { this("666666", 25000, name, email, phone); } public Account() { this("55555", 20000, "bob", "bob@123.com", "12345678901"); } }
以上代码中的Account类中有三个构造函数(overload),其中两个构造函数通过this()方法调用了具有完整形参列表的那个构造函数来完成对应的初始化过程,这样减少了冗余的初始化代码。这种方式也可以让我们灵活的设置需要被初始化实例的默认值。
(Tip:this()只能够在构造函数中使用,而且如果使用this(),那么this()语句必须在对应构造函数的第一行,否则会报错!this()往往在构造函数间互相调用时(constructor chaining)使用,其目的就是为了减少重复的代码。它的另外一个“兄弟”super()的作用则是用于子类构造函数调用父类构造函数(而且是唯一方式),这将在后面介绍)
2. 继承
继承其实就是is-a的关系,例如:Dog is Animal.狗是动物,说明狗具有动物的一些共有的特性,所以我们能够称之为动物。在JAVA的世界里,我们就可以通过类之间的继承来描述这种关系。例如:
首先,我们定义一个父类Animal,该类中具有一些实例属性用于描述“动物”这个集合所具有的的某些特征:
public class Animal { private String name; private int brain; private int body; private int size; private int weight; public Animal(String name, int brain, int body, int size, int weight) { this.name = name; this.brain = brain; this.body = body; this.size = size; this.weight = weight; } public void eat() { System.out.println("Animal.eat() called"); } public void move() { System.out.println("Animal.move() called"); } public String getName() { return name; } public int getBrain() { return brain; } public int getBody() { return body; } public int getSize() { return size; } public int getWeight() { return weight; } }
众所周知,狗是动物,所以我们可以在JAVA中定义一个Dog类作为Animal的子类,这需要用到extends关键字:
public class Dog extends Animal { private int eyes; private int legs; private int tail; private int teeth; private String coat; public Dog(String name, int body, int brain, int size, int weight, int eyes, int legs, int tail, int teeth, String coat) { super(name, body, brain, size, weight); // super(name, 1, 1, size, weight); this.eyes = eyes; this.legs = legs; this.tail = tail; this.teeth = teeth; this.coat = coat; } private void chew() { System.out.println("Dog.chew() called"); } @Override public void eat() { System.out.println("Dog.eat() called"); chew(); super.eat(); } }
Dog虽然是Animal,但是它也有独有的一些特征,而这些特征我们就可以单独定义在Dog类的属性(field)中。
在Dog的构造函数中,super()则是用于调用父类Animal的构造函数。
在子类Dog中,我们可以重写(override)父类的同名方法,这里简单介绍一下重写的定义:1.方法名相同 2.形参列表必须相同 3.返回值的类型也必须相同。总而言之,就是重写的方法的方法签名必须完全一致!
每当我们重写方法时,都建议在方法签名上方添加一个annotation:@override。这个标记会告诉JAVA编译器,这是一个重写的方法,当我们的方法实现不符合重写的规则时,编译器会报错告知我们,这也是一个比较好的编码实践。
在上面Dog类中重写的eat方法中,super.eat()是用于调用父类中对应的同名方法eat,super.XXX()形式。
子类Dog中重写了eat方法后,当我们实例化生成dog对象的时候,调用的eat方法,将会是子类中重写后的方法。例如:
public class Main { public static void main(String[] args) { Animal animal = new Animal("Animal", 1, 1, 5, 5); Dog dog = new Dog("Yorkie", 8, 20, 2, 4, 1, 20, "long silky"); dog.eat(); } }
3.Class,Instance,Object,Reference区别
简而言之:
1. Instance是Class通过new关键字创建的实例,Class可以理解为蓝图(blueprint),而Instance可以理解为根据蓝图创建出来的具体东西;
2. 创建的Instance往往就是具体的对象,而对象是需要具体的变量去承载的,在runtime阶段,创建的未被回收的对象就会常驻在内存中,而对应的内存地址就是Reference,可以理解为Object的地址(Location)。赋值过程中的变量所指向的就是对应对象的地址。
3.变量指向的仅仅是对象地址而已,所以同一变量所指向的地址是可以改变的,也就是指向的Reference发生了变化,从而指向了其他具体的对象。
4. this关键字与super关键字
super用于获取(access)或调用(call)父类中的成员(变量和方法):
(1)super(…):子类构造函数中调用父类构造函数;
(2)super.XXX():子类中调用父类的同名方法;
this则是用于调用当前类的成员(变量和方法)。特别是当我们在初始化实例变量(field)时,往往会采用与实例变量相同的局部变量名,this.XXX = XXX; this关键字能够帮助我们很好的区分它们:
(1)this():用于在当前类的构造函数重载(overload)中,调用另一个构造函数;
(2)this.XXX:指向当前类中的实例属性;
super()和this()的调用,都必须在对应构造函数中的第一行!!!而且在同一个构造函数中,这俩兄弟可是“势如水火”,是不能同时出现的哦。
(Note:this和super可以被用在类的任何地方,除了静态范围(static areas),例如静态块级作用域(static block)和静态方法(static method)中,任何尝试使用都会导致编译时错误(compile-time errors))。
抽象类(abstract class)同样具有构造函数,虽然你无法通过new关键字实例化一个抽象类,但是我们可以在继承它的子类中,去实例化对象,在此过程中,抽象类的构造函数同样会被调用。
5.重载(overload)和重写(override)
这俩哥们也是初学者在学习JAVA的过程中比较容易搞混淆的(比如我=.=)。那么下面将简单介绍一下这俩概念的区别:
(1)方法重载(method overloading):在一个类中,具有不同参数列表的同名方法(返回值的类型不做要求)可以称为方法重载。
方法重载往往是为了减少同一类中近似方法(功能近似)出现重复代码,而且允许使用同样的方法名(这样就可以不用记忆多个完成近似功能方法的不同方法名,只需要确保不同方法的形参列表不同即可)。
方法重载与多态这个概念并没有太多的关系,但是许多的JAVA开发者都常常会称之为编译时多态(Compile Time Polymorphism),这个编译时多态实际上指的就是,具体该调用哪个方法,在代码的编译阶段就可以确定。因为编译器决定调用哪个同名方法,是根据方法签名(方法名,返回值类型,形参列表)来的,而overload的方法在明确不同的形参列表后,具体该调用哪个方法在编译阶段就能够完全确定下来。
我们不仅可以重载实例方法,还可以重载静态方法哦~
(2)方法重写(method overriding):子类中的某个方法,如果其与父类中的某个方法有着相同的方法签名,那么我们可以称之为方法的重写。
当我们在定义子类时使用关键字extends继承某个父类的时候,该子类将会自动获得所有父类的实例方法(访问修饰符得是public,当然如果是protected,则该子类必须与父类在同一个包下)。而我们可以通过在子类中重写父类中的同名方法,来定义不同的行为(当然override的过程中,也可以通过super.XXX()的方式调用父类的同名方法)。
方法重写也被许多的JAVA开发者称为运行时多态(Run Time Polymorphism)或者是动态方法分发(Dynamic Method Dispatch),因为重写的方法具体该调用哪一个,这个必须由JVM在运行时确定。
我们只能重写父类中的实例方法,不能够重写静态方法(但是可以重新声明)。
实际上,方法重写的定义不仅仅要求方法签名完全一致,它还要求:
1.重写的方法不能有更严格的访问修饰符;(例如:如果某个父类的实例方法是protected的,那么在子类中重写该方法时,不能定义为private的,而定义为public则是允许的);
2.不能够抛出一个新的或者更加广泛的(new or broader)确定的异常;
6.静态方法与实例方法,静态属性与实例属性
(1)静态方法与实例方法:
1.静态方法用static进行声明;
2.静态方法不能够直接的获取(access)实例方法与实例属性。
3.静态方法中是不能够使用this关键字的(由(2)得来);
4.静态方法的使用场景往往是:方法中的操作不需要任何来自于当前类中实例的数据(也可以理解为来自于this)。这样的方法也应该别声明为静态的!
5.静态方法的调用方式是:className.methodName()的方式(不同类之间调用)以及methodName()的方式(同一个类的调用);
1.实例方法属于类的实例;
2.调用实例方法前,必须先实例化得到Instance对象后,再进行调用;
3.实例方法可以直接的获取(access)实例方法和实例属性;
4.实例方法可以直接的获取(access)静态方法和静态属性;
那么,我们在什么时候该把方法定义为静态的呢?附图:
(2)静态属性与实例属性
1.静态属性也是静态成员变量;
2.所有的类实例都会共享静态变量,一旦某个实例中改变了静态变量,其他所有实例获取到的静态变量都会发生同样的变化;
1.所有的实例都有对应类的实例属性的副本;(一个实例中的某个实例属性发生改变并不会影响其他实例);
2.实例属性代表了实例的状态;
7.组合
与继承相对应的,面向对象中还有组合的概念,组合描述的其实就是has-a的关系,比如:PC has a monitor; 也就是说,某一个类的实例可以作为另一个类的属性,可以说这个类由另一个(一些)类组成;
Java中是仅支持单继承的,但是一个类可以由多个类组合而成,一台电脑(PC类)可以由显示屏,主板,显卡,CPU等其他类构成。例如:
首先,我们可以创建一个主板类Motherboard:
public class Motherboard { private String model; private String manufacturer; private int ramSlots; private int cardSlots; private String bios; public Motherboard(String model, String manufacturer, int ramSlots, int cardSlots, String bios) { this.model = model; this.manufacturer = manufacturer; this.ramSlots = ramSlots; this.cardSlots = cardSlots; this.bios = bios; } public void loadProgram(String programName) { System.out.println("Program " + programName + " is now loading..."); } public String getModel() { return model; } public String getManufacturer() { return manufacturer; } public int getRamSlots() { return ramSlots; } public int getCardSlots() { return cardSlots; } public String getBios() { return bios; } }
然后是显示器类Monitor,但是在定义它之前,我们还需要定义一个分辨率的类Resolution:
public class Resolution { private int width; private int height; public Resolution(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } }
Monitor类:
public class Monitor { private String model; private String manufacturer; private int size; private Resolution nativeResolution; public Monitor(String model, String manufacturer, int size, Resolution nativeResolution) { this.model = model; this.manufacturer = manufacturer; this.size = size; this.nativeResolution = nativeResolution; } public void drawPixelAt(int x, int y, String color) { System.out.println("Drawing pixel at " + x + "," + y + " in colour " + color); } public String getModel() { return model; } public String getManufacturer() { return manufacturer; } public int getSize() { return size; } public Resolution getNativeResolution() { return nativeResolution; } }
接着是Dimensions类和Case类:
public class Dimensions { private int width; private int height; private int depth; public Dimensions(int width, int height, int depth) { this.width = width; this.height = height; this.depth = depth; } public int getWidth() { return width; } public int getHeight() { return height; } public int getDepth() { return depth; } }
public class Case { private String model; private String manufacturer; private String powerSupply; private Dimensions dimensions; public Case(String model, String manufacturer, String powerSupply, Dimensions dimensions) { this.model = model; this.manufacturer = manufacturer; this.powerSupply = powerSupply; this.dimensions = dimensions; } public void pressPowerButton() { System.out.println("Power button pressed"); } public String getModel() { return model; } public String getManufacturer() { return manufacturer; } public String getPowerSupply() { return powerSupply; } public Dimensions getDimensions() { return dimensions; } }
以上类能够通过组合的形式构成PC类:
public class PC { private Case theCase; private Monitor monitor; private Motherboard motherboard; public PC(Case theCase, Monitor monitor, Motherboard motherboard) { this.theCase = theCase; this.monitor = monitor; this.motherboard = motherboard; } public Case getTheCase() { return theCase; } public Monitor getMonitor() { return monitor; } public Motherboard getMotherboard() { return motherboard; } }
可以看到,我们需要在初始化PC类的时候,先初始化Case,Monitor,Motherboard类的实例,再将其作为PC类的实例属性,main()中初始化代码如下:
public class Main { public static void main(String[] args) { Dimensions dimensions = new Dimensions(20, 20, 5); Case theCase = new Case("2208", "Dell", "240", dimensions); Monitor theMonitor = new Monitor("27inch Beast", "Acer", 27, new Resolution(2540, 1440)); Motherboard theMotherboard = new Motherboard("BJ-200", "Asus", 4, 6, "v2.44"); PC thePC = new PC(theCase, theMonitor, theMotherboard); thePC.getMonitor().drawPixelAt(1500, 1200, "red" ); thePC.getMotherboard().loadProgram("Windows 1.0"); thePC.getTheCase().pressPowerButton(); } }
组合起来的PC类如何去调用组成它的,比如Monitor类中的成员方法呢?实际上很简单,可以直接通过实例thePC获取组成它的monitor实例,再通过该实例去调用其成员方法drawPixelAt。
在实际的编码中,组合的运用应当比继承更加广泛。
8. 封装(Encapsulation)
封装也是一个十分重要的概念。封装有以下几个好处:
(1)封装避免调用类的代码能够随意的取修改类中的实例变量等,当我们创建一个类的时候,往往希望类的使用者能够按照我们希望的方式去使用这个类,而封装能够很好的保证这一点。这也是为什么getter和setter以及constructor存在原因。private访问控制符是封装的唯一保证。通过getter和setter,我们能够对使用者传入的参数做许多的事情,譬如参数校验等等。
(2)封装能够解耦代码,例如:
public class Animal { public int eyes; public int feet; //...... } Animal an = new Animal(); an.eyes = 2;
假如我们通过以上未封装的类,通过直接在实例上赋值的方式初始化类,那么一旦我们的Animal类中的成员发生修改时,就需要同步的修改使用Animal类的代码。如果上例中的eyes需要改为animalEyes,不仅需要修改类内部的代码,还需要修改实例化的赋值代码。这里只是一个简单的例子,所以改动的地方不多,但是一旦在工程化的代码中使用这种未封装好的代码,那需要修改的地方可就不是一点点了。而以下的模式,则可以很好的避免这个问题:
public class Animal { private int eyes; private int feet; //...... public Animal(int eyes, int feet) { this.eyes = eyes; this.feet = feet; } } Animal an = new Animal(2, 2);
类中的代码需要修改时,使用类的代码并不需要做任何的改动。
总而言之,封装使得我们构造的类成为一个黑盒子般,其他有坏心思的人,不能够随意的去修改黑盒子中的内容,要想使用黑盒子,就必须按照我们预先的设定去使用(说明书)。而且,一旦黑盒子内部需要修改或升级,我们只需要修改黑盒子内部的代码,大多数情况下,并不需要修改说明书(使用方式)。
9.多态(Polymorphism)
多态是同一个行为具有多个不同表现形式或形态的能力。多态性是对象多种表现形式的体现。一般这么说肯定不太明白,还是用一个例子来进行说明:
首先,我们需要构造一个Movie类,它表示所有的电影,Movie类中只有一个私有成员(name:电影名),还有两个公有方法,一个用于获取电影名,另一个则是电影内容的大致描述(plot),Movie类的代码如下:
class Movie { private String name; public Movie(String name) { this.name = name; } public String getName() { return this.name; // or return name; } public String plot() { return "No plot here!" } }
那么我们需要在这个类的基础上去派生其他更加具体的类,比如电影大白鲨(Jaws)类:
class Jaws extends Movie { public Jaws() { super("Jaws"); } @Override public String plot() { return "A shark eats lots of people!" } }
此时,我们创建另一个类,这个类我们“忘记”去重写它的plot方法:
class Forgetable extends Movie { public Forgetable() { super("Forgetable"); } // No plot method }
然后我们去具体的实例化它们,然后看一下会出现什么样的行为:
public class Main { public static void main(Stringp[] args) { Movie Jaws = new Jaws(); System.out.println(Jaws.plot()); Movie Forgetable = new Forgetable(); System.out.println(Forgetable.plot()); } }
最终,返回的结果如下:
其实以上代码展示的就是多态的特性。而且,我们也可以发现,多态需要以下三个条件:
(1)继承(extends);
(2)重写(override);
(3)父类引用(Movie Jaws)指向子类对象(new Jaws());
通过多态,我们继承某个父类的子类能够自行定制与父类相同行为的不同方式。而如果子类中没有自行定义,那么子类实例将会使用与父类相同的行为。