1、前言
相信在学习Java的时候,大家听到最多的两个词就是类和对象了。可见类和对象的重要性,Java本就是一门面向对象的语言,深入理解面向对象首先要理解类和对象是什么。
实际上,可以将类看作是对象的载体,类中定义了对象所具有的所有功能。而对象则是类的具体一个实例,研究问题一般是从对象入手,而不是从类。想要学好Java,必须要非常熟练的掌握类和对象,这样可以更深的去理解面向对象的开发思想。
2、面向对象
程序开发的初期是使用结构化语言进行开发的,采用结构化语言开发,开发的周期长,并且产品质量也不尽人意,于是人们开始将另一种开发思想引入到了程序中,这种思想就是面向对象思想。面向对象思想是人类最自然的一种思考方式,将所有预处理的问题抽象为对象,通过研究这些对象具有的属性和行为来解决这些对象所面临的实际问题,于是程序开发中面向对象的设计概念就产生了,面向对象设计实际上是对现实世界对象进行的建模操作。
在现实世界中,随处可见的事物都是对象,比如一个人,一台电脑,一个茶杯,这些都可视为对象。一般会将对象分为两个部分,即静态部分和动态部分。静态部分即是不能动的部分,称为属性。以人为例,人的属性有身高、体重、年龄、性别等。而人这个对象是会动的,可能会执行一些列的动作,那么这些可以动的部分称为行为。那么通过了解对象的属性和行为来进一步了解对象。
面向对象的设计思想就是以对象来思考问题,先将现实世界的实体类抽象为对象,然后考虑这个对象具有的属性和行为。现在想研究一个人从出生到死亡的问题,那么从面向对象的思想出发:
(1)首先抽象出对象,那么抽象出来的对象就是一个人。
(2)然后研究这个对象具有的属性,比如这个人的肤色、性别、身高、体重、长相等。
(3)然后研究这个对象的行为,比如这个人要吃饭、睡觉等一系列必须的动态行为。
(4)确定这个对象的属性和行为后,那么该对象的定义就完成了,可以根据这个人具有的特性来具体研究从出生到死亡的具体过程了。
实际上,所有的人都具有以上的属性和行为,那么可以将这些属性和行为封装起来用来描述人这个类。因此,类实质上就是封装对象属性和行为的载体,而对象是类抽象出来的一个具体实例,用以下图示说明:
将一个人的通用属性和必须行为封装起来,用来描述人这一类具有相同属性和行为的事物。
3、类
类是同一类事物的统称,如果将现实世界中的一个事物抽象成对象,那么类是这类对象的统称,类是构造对象的模板,具有相同特性和行为的一类事物称为类。对象是符合某个类的定义所产生出来的实例。面临实际问题时,一般需要实例化对象来解决,并不能从类的层面进行解决。**类是封装对象属性和行为的载体,具有相同属性和行为的一类事物才称为类。**在Java中,类中对象的行为以方法来定义,对象的属性以成员变量来定义,类封装了对象的属性和方法。
3.1、类的定义
定义类的关键字是class,如下:
public class Person {
}
3.2、成员变量
Java中对象的属性也叫成员变量,以下代码说明:
public class Person {
//成员变量,也叫属性
private String name;
//成员变量
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
以上的name和sex都是成员变量,也叫属性。可以初始化,也可以选择不初始化。
3.3、成员方法
成员方法对应类对象的行为,以下代码说明:
public class Person {
private String name;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public void sleep() {
System.out.println("人需要睡觉!");
}
public String say(String mess) {
String name = "ycz";
return "Hello " + mess + name;
}
}
以上的方法都是成员方法,成员方法也叫实例方法,是相对于对象而言的。成员方法可以有返回值可以没有返回值,可以有参数也可以无参数,成员方法中定义的变量叫做局部变量,只在方法内有效,如上面say方法里定义的name变量。
3.4、权限修饰符
权限修饰符的作用是控制对类、类成员变量、类成员方法的访问。权限修饰符有:public、private、protected还有一个默认的,就这4种,说明如下:
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- public ::对所有类可见。使用对象:类、接口、变量、方法
- protected: 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
如下表:
将权限修饰符理解为对资源的权限控制。以下代码:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
void showMe(String name) {
System.out.println("My Name is " + name);
}
protected void like(String song) {
System.out.println("最喜欢的歌是:" + song);
}
}
3.5、局部变量
成员方法内部定义的变量就是局部变量,局部变量的有效范围是方法内部,方法之外是无效的。代码如下:
public class Person {
public int add() {
int a = 2, b = 3;
return a + b;
}
public int div() {
int a = 20, b = 5;
return a / b;
}
}
局部变量只在方法内有效,所以在另一个方法中即使定义了上一个方法中相同的变量也是允许的,如上面的a和b,最能说明的应该是for循环了。
3.6、this关键字
用一个例子说明:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person getPerson() {
return this;
}
}
Java中this关键字代表本类对象的引用,this关键字被隐式的用于引用对象的成员变量和方法。如上面的setName方法中,this.name = name,这句代码的含义是将形参name的值赋给成员变量name,可以理解为形参值覆盖成员变量值。如getPerson方法,需要返回一个Person类的对象,返回了this,那么this就是引用Person类的一个对象。
3.7、构造方法
构造方法是一个与类同名的方法,对象的创建需要通过构造方法来完成,每当实例化对象时,类就会自动调用构造方法。构造方法的特点如下:
- 无返回值。
- 与类同名。
如果在类中没有定义构造方法,那么编译器会自动创建一个无参的构造方法,手动定义无参方法也可以。注意的是如果定义了一个有参的构造方法,那么它会覆盖无参构造方法,如果需要用到无参构造方法,只能手动再定义添加,编译器不会再自动添加了。以下代码说明:
public class Person {
private String name;
public Person() {
System.out.println("调用无参构造方法!");
}
public Person(String name) {
this.name = name;
System.out.println("调用有参构造方法!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试类:
public static void main(String[] args) {
Person p0 = new Person();
}
public static void main(String[] args) {
Person p0 = new Person();
Person p2 = new Person("ycz");
}
3.8、静态变量、常量、方法
静态的需要加static修饰。有时候多个类需要在同一个内存区域中共享一个数据,比如常量PI,多个类都可能使用它,没必要在每个类中都定义这个常量,每个类都定义的话系统会分配到不同的内存空间,这样就造成了资源的浪费。这时可以将这个常量PI定义成静态常量,也就是加static修饰。同样的静态成员、静态方法也是这个道理。一般调用成员属性和成员方法是通过对象.属性/方法来定义,定义成静态的之后,属于类所有,要通过类名.常量名/属性名/方法名来调用。
代码如下:
public class Person {
// 静态常量
private static final double PI = 3.14;
// static String name;//成员变量一般不会定义成静态的
private String name;
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 静态方法
public static void m1() {
System.out.println("静态方法!");
System.out.println("输出静态常量:" + Person.PI);
}
}
测试:
public static void main(String[] args) {
// 调用静态方法
Person.m1();
}
静态方法有以下几点需要注意的:
- 静态方法中不能使用this关键字。
- 静态方法中只能调用静态方法,无法调用非静态方法。
在方法内部是不可以有static关键字的,Java中不允许。
3.9、静态代码块
如果希望在执行类之前先完成类的初始化工作,可以使用静态代码块,如下:
public class Person {
private static final String MESS_0;
private static final String MESS_2;
private static final String MESS_3;
static {
MESS_0 = "下午好啊,";
MESS_2 = "中饭吃的什么?";
MESS_3 = "下午茶喝了没?";
}
public void ask() {
System.out.println(MESS_0.concat(MESS_2).concat(MESS_3));
}
}
测试:
public static void main(String[] args) {
Person p0 = new Person();
p0.ask();
}
如上,本来定义静态常量要完成初始化的,即定义时给常量赋值,而静态代码块允许只定义,后在static块中统一赋值。
需要注意如下几点:
- 静态代码块中允许定义静态属性,调用静态方法。
- 允许定义局部变量和常量。
- 不能访问实例属性和方法以及定义方法。
补充:要注意静态代码块属于类,无关对象,随着类的加载而加载,在静态属性初始化之后与调用构造方法之前加载,通常是在代码块中赋值常量,调用静态方法属性等,想在产生类对象之前加载某些信息时可选择使用静态代码块。
3.10、方法重载
方法重载发生在一个类的内部,要求修饰符、方法名称、返回值必须相同,只是参数列表不同。以下用代码演示:
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void say(String mess) {
System.out.println("对方回了一句:" + mess);
}
public void say(String mess, String pic) {
System.out.println("对方回了一句:" + mess + ",并且向你发了一张" + pic + "动态图!");
}
}
在以上代码中,构造方法进行了重载:
public Person() {
}
public Person(String name) {
this.name = name;
}
在实例化对象的时候,会根据参数列表来决定调用哪一个构造方法。
普通的成员方法也进行了重载:
public void say(String mess) {
System.out.println("对方回了一句:" + mess);
}
public void say(String mess, String pic) {
System.out.println("对方回了一句:" + mess + ",并且向你发了一张" + pic + "动态图!");
}
调用此方法的时候,会根据传入的参数来决定调用哪一个方法。
测试:
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.say("你好");
p.say("你好", "揉狗头");
}
}
控制台输出如下:
4、对象
Java是一门面向对象的程序设计语言,对象从类中抽象出来,所有问题都是通过对象来处理,对象可以操作类的属性和方法,因此了解对象的生成、操作和消亡很重要。
4.1、对象的创建
Java中通过new操作符来创建对象,每次实例化一个对象都会自动调用一次构造方法,也就是使用new操作符来调用构造方法完成对象的实例化,如下:
public class Person {
private String name;
// 无参构造方法
public Person() {
}
// 有参构造方法
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
Person p0 = new Person();
Person p2 = new Person("aaaa");
}
创建对象时,可以选择定义的哪一个构造方法来创建。
对象创建出来时,其实是一个对象的引用,这个引用在内存中分配了存储空间。通过new创建出来的对象,每个对象都是相互独立的,在内存中占据独立的内存地址,并且每个对象都有自己的生命周期,当生命周期结束时,对象会成为垃圾,JVM自带的垃圾回收机制会处理这些过期对象。JVM以后会在另外的文章中介绍。
4.2、访问对象的属性和方法
对象创建出来以后,可以通过对象.成员/方法来访问这些属性和方法,如下:
public class Test {
private int i = 10;
public Test() {
}
public void m1() {
System.out.println("调用m1方法!");
for (int i = 1; i <= 5; i++) {
System.out.println("现在i的值是:" + i);
}
}
public static void main(String[] args) {
Test t0 = new Test();
Test t2 = new Test();
System.out.println("t0对象调用:" + t0.i++);
System.out.println(t0.i);
t0.m1();
System.out.println("-------------------");
t2.i = 30;
System.out.println("t2对象调用:" + t2.i);
t2.m1();
}
}
每个对象是独立的,因此对象的属性和行为也是独立的,每个的对象属性只能通过它自己来修改,无法通过其它对象来修改。如果将成员变量定义为静态的:
public class Test {
private static int i = 10;
public Test() {
}
public void m1() {
System.out.println("调用m1方法!");
for (int i = 1; i <= 5; i++) {
System.out.println("现在i的值是:" + i);
}
}
public static void main(String[] args) {
Test t0 = new Test();
Test t2 = new Test();
System.out.println("t0对象调用:" + t0.i++);
System.out.println(t0.i);
t0.m1();
System.out.println("-------------------");
t2.i = 30;
System.out.println("t2对象调用:" + t2.i);
t2.m1();
System.out.println(Test.i);
}
}
如果是将成员变量定义为静态的,那么任何一个对象都能修改它,基本上是不会这样做的,没有将静态成员定义为静态的。
4.3、对象的引用
Java中虽然一切都看作对象,但是要明白操作符实际上只是一个引用而已。比如:
Person p0 = new Person();
这个p0其实只是一个引用,指向内存中为对象分配的地址。p0只是对象内存地址的一个引用,并非对象,但是这种区别基本可以忽略,p0包含Person对象的一个引用,可以认为p0就是Person的一个对象。
4.4、对象的比较
Java中对象的比较有==和equals这两种。
代码:
public static void main(String[] args) {
String s0 = new String("ycz");
String s2 = new String("ycz");
String s3 = s0;
System.out.println(s0 == s2);
System.out.println(s0 == s3);
System.out.println(s0.equals(s2));
}
输出结果:
==比较的是对象的引用地址是否相同,这里的equals方法比较的是字符串的内容是否相等,String类的源码如下:
String类是重写了Object的equals方法,源码中可以看出,String类规定的equals方法就是比较串的内容是否相等,内容相等就认为这两个String类对象相等了。事实上,每个类的比较规则都应该重新定义自己的equals方法,在该方法中定义相等的规则。
4.5、对象的销毁
每个对象都有生命周期,当生命周期结束后,分配给该对象的内存地址就会被回收。其他语言比如c可能需要手动回收废弃对象,但是在Java中无需手动,Java有自己的一套垃圾回收机制,垃圾回收器会回收无用且占用内存的资源,从而释放内存。垃圾回收器只会回收它视为垃圾的对象,包括:
- 对应引用超过其作用范围,这个对象会被视为垃圾,将进行回收。
- 将对象的值赋为null,这个对象会被视为垃圾,将进行回收。
这里就简单的说这两种,后面在JVM篇章中会详细说明哪些对象会被视作垃圾进行回收。
尽管垃圾回收机制很完善,但垃圾回收机制只能回收那些由new操作符创建的对象。有的对象并不是new的,但也在内存中占了一块内存区域,对于这种,垃圾回收器无法识别,也就无法清理了,这时,需要我们手动调用finalize()方法,通知垃圾回收器来进行回收。
注意:就算调用了finalize()方法,也不能保证垃圾回收器一定会回收对象成功。
垃圾回收不受人为控制,Java提供了System.gc()方法来强制启动垃圾回收器,同样的,这也只是用户期待的,并不能保证垃圾回收成功。
5、类的继承
继承是发生在类与之间的关系。继承的类称为子类,被继承的类称为父类或超类。继承是面向对象开发思想中一个重要的概念,继承使整个程序具有一定的弹性,可以复用一些已经定义好的类,减少开发周期,提高软件的可维护性和扩展性。子类继承父类后,可以使用父类的属性和方法,也可以增加原来父类不具备的属性和方法,可以重写父类中的某些方法,重写也叫覆盖。继承的关键字是extends。
5.1、继承的语法
访问修饰符 class 子类名 extends 父类名{}
比如:
public class A extends B{
}
5.2、继承示例
下面使用代码来说明,先定义超类Vehicle:
/*
* 交通工具类
*/
public class Vehicle {
private String brand;// 品牌
private String color;// 颜色
private int speed = 0;// 速度,初始化为0
// 构造方法
public Vehicle() {
System.out.println("调用父类的无参构造方法!");
}
public Vehicle(String brand, String color, int speed) {
this.brand = brand;
this.color = color;
this.speed = speed;
System.out.println("调用父类的有参构造方法!");
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getSpeed() {
return speed;
}
public void setSpeed(int speed) {
this.speed = speed;
}
// 自定义方法
public void run() {
System.out.println("汽车速度比自行车快!");
}
protected Vehicle test() {
return new Vehicle();
}
}
然后定义子类Car:
/*
* 汽车类,继承自交通工具类
*/
public class Car extends Vehicle {
private int loader;// 载人数
// 构造方法
public Car() {
System.out.println("调用无参构造方法!");
}
// 构造方法
public Car(int loader) {
this.loader = loader;
}
// 构造方法
public Car(String brand, String color, int speed, int loader) {
super(brand, color, speed);
this.loader = loader;
System.out.println("调用有参构造方法!");
}
public int getLoader() {
return loader;
}
public void setLoader(int loader) {
this.loader = loader;
}
// 覆盖父类的run方法
@Override
public void run() {
System.out.println(this.getBrand() + "牌的" + this.getColor() + "汽车正在国道上行驶!");
System.out.println("行驶速度为:" + this.getSpeed() + "km/h,载人数为:" + this.getLoader());
}
// 覆盖父类的test方法
@Override
protected Car test() {
return new Car();
}
// 添加的新方法
public void stop() {
System.out.println("汽车没油了,无法继续行驶!");
}
}
测试:
public class Test {
public static void main(String[] args) {
Vehicle v = new Vehicle("宝马", "red", 75);
v.run();
System.out.println("---------------------------------");
Car c = new Car("丰田", "black", 90, 6);
c.run();
c.stop();
}
}
执行,控制台输出:
可以看到创建子类对象时,会先创建父类对象,就是说调用子类的构造方法时会自动先调用父类的构造方法。
5.3、继承的特点和限制
特点:
- 一个类可被多个类继承,即一父多子。
- 一个子类只能有一个直接父类。
- 继承具有传递性。
- final关键字修饰的类不能被继承。
- 子类拥有父类非private的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以重写父类中的方法。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
限制:
- 默认父类的成员属性和方法都可被子类继承。
- 私有属性和方法无法在子类中直接访问。
- 构造方法不能被继承。
- 上转型对象不能访问子类中新添加的属性方法,即父类对象无法访问子类的扩展方法。
- final修饰的方法不能在子类中重写。
图示如下:
5.4、方法重写
方法重写只会发生在子类继承父类时,方法重写也叫覆盖。子类将父类的成员方法名称保留,参数一致,可以更改方法的权限,但是权限范围只能不变或者扩大,就是子类中重写的方法权限不能低于父类中被重写的方法权限,成员方法的返回值类型也可以更改,但有限制,就是子类中重写方法的返回值必须是父类中被重写方法返回值的子类。下面举例进行说明。
这是父类中的方法:
// 自定义方法
public void run() {
System.out.println("汽车速度比自行车快!");
}
protected Vehicle test() {
return new Vehicle();
}
这是子类中重写后的方法:
// 覆盖父类的run方法
@Override
public void run() {
System.out.println(this.getBrand() + "牌的" + this.getColor() + "汽车正在国道上行驶!");
System.out.println("行驶速度为:" + this.getSpeed() + "km/h,载人数为:" + this.getLoader());
}
// 覆盖父类的test方法
@Override
protected Car test() {
return new Car();
}
可以看到,run方法重写只是实现内容改变。而test方法重写不仅改变了方法的修饰符,还更改的了方法的返回类型。
注意:方法重写发生在类与之间,也就是有继承关系时。
5.5、方法重写与方法重载的区分
可以从以下两点来区分:
- 方法重写发生在继承时,方法重载发生在一个类的内部,因为类中不允许有完全一样的两个方法,因此相同名称的方法必须参数不同。
- 方法重写只需保持方法名、参数与父类中的一致即可,方法的修饰符、返回值可以修改。方法重载要求修饰符、方法名、返回值都必须一致,要求参数列表不同。
5.6、super关键字
与this类似,super关键字只存在子类中,指向父类对象。
如果是在构造方法中,表示调用父类构造器,且要位于子类构造器的第一行。默认在子类中是隐藏的,但是如果父类中定义的构造器带参数,那么子类中必须要显示出super关键字,否则会报错。
父类构造方法:
public Vehicle(String brand, String color, int speed) {
this.brand = brand;
this.color = color;
this.speed = speed;
System.out.println("调用父类的有参构造方法!");
}
子类构造方法:
// 构造方法
public Car(String brand, String color, int speed, int loader) {
super(brand, color, speed);
this.loader = loader;
System.out.println("调用有参构造方法!");
}
以上在子类的构造方法中使用super调用了父类的构造方法,有参数,那么super必须放在第一行。
父类中普通的成员方法:
public void test2() {
System.out.println("111");
}
子类中重写调用父类中的方法:
@Override
public void test2() {
super.test2();
}
6、抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类,也就是说抽象类除了被继承,没有任何意义,设计抽象类出来就是为了被继承。
6.1、抽象类的定义
抽象类使用abstract关键字来定义,如下:
public abstract class People {
}
只是在定义普通类的基础上加了一个abstract关键字。
6.2、抽象类的特点
- 抽象类通常包含抽象方法,也可包含非抽象方法。换句话说,抽象类中不一定有抽象方法,但有抽象方法的类一定是抽象类。
- 抽象类不能使用final关键字修饰。
- 抽象类自身不能实例化,需通过子类。
- 抽象类是用作继承的,否则没有存在的意义。
- 定义抽象方法时,不能有方法体。
- 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
- 抽象类的子类必须给出抽象类中所有抽象方法的具体实现,除非该子类也是抽象类。
6.3、抽象类使用示例
以下用一个例子来说明抽象类的使用。
先定义一个抽象类Animal:
/*
* 抽象类,动物类
*/
public abstract class Animal {
//定义一个抽象方法
public abstract void run();
//无参构造方法
public Animal() {
System.out.println("调用动物类的无参构造方法!");
}
}
再定义一个抽象类MammalAdaptor,该类直接继承自Animal类:
/*
* 抽象类,哺乳动物,继承自Animal
*/
public abstract class Mammal extends Animal {
// 定义两个抽象方法
public abstract void eat();
public abstract void location();
//构造方法
public Mammal() {
System.out.println("调用哺乳动物类的无参构造方法!");
}
}
再定义一个适配器,适配器直接继承自Mammal:
/*
* 哺乳动物类的适配器
*/
public class MammalAdaptor extends Mammal {
@Override
public void eat() {
// TODO Auto-generated method stub
}
@Override
public void location() {
// TODO Auto-generated method stub
}
@Override
public void run() {
// TODO Auto-generated method stub
}
}
定义两个子类Dog和Cat,直接继承自MammalAdaptor类:
/*
* 继承自适配器
*/
public class Dog extends MammalAdaptor {
@Override
public void eat() {
System.out.println("狗蛋只吃鸡腿和大骨头!");
}
@Override
public void run() {
System.out.println("狗蛋跑的比二猫子快!");
}
}
/*
* 继承自适配器
*/
public class Cat extends MammalAdaptor{
@Override
public void eat() {
System.out.println("二猫子并不想吃老鼠,只吃鱼!");
}
}
这里的继承关系有3重,Dog类和Cat类继承自MammalAdaptor类,MammalAdaptor类继承自Mammal类,Mammal类继承自Animal类,Java是允许多重继承的,但是不允许多继承,注意区分。这里用到了适配器,下面会进行说明。
测试:
public static void main(String[] args) {
Mammal dog = new Dog();
dog.eat();
dog.run();
System.out.println("------------------------");
Cat cat = new Cat();
cat.eat();
}
输出结果:
6.4、适配器
如果我想使用一个抽象类中的方法,但是我仅仅只会用其中的一个抽象方法,而抽象类中有不止一个抽象方法,我又不想因为使用一个方法而将全部的抽象方法都实现,那怎么办呢?这时可以通过适配器来实现,适配器其实是一个普通的类,以下进行说明:
public abstract class People {
public abstract void m1();
public abstract void m2();
public abstract void m3();
public abstract void m4();
public abstract void m5();
}
我只想使用这个抽象类中的m3方法,但是我又不想实现其他4个抽象方法,通过一个普通类来过渡,这个类继承这个抽象类,并对全部的抽象方法进行一个空实现,如下:
/*
* 作为适配器使用
*/
public class Adapter extends People {
@Override
public void m1() {
// TODO Auto-generated method stub
}
@Override
public void m2() {
// TODO Auto-generated method stub
}
@Override
public void m3() {
// TODO Auto-generated method stub
}
@Override
public void m4() {
// TODO Auto-generated method stub
}
@Override
public void m5() {
// TODO Auto-generated method stub
}
}
然后直接继承这个适配器就行了,选择需要的方法重写:
public class Man extends Adapter {
@Override
public void m3() {
System.out.println("男人一般比女人高!");
}
}
测试:
public static void main(String[] args) {
People man = new Man();
man.m3();
}
事实上是用到了多重继承,适配器作为中间类去继承父类,然后要使用的类继承适配器,适配器只是起到一个桥梁的作用。这种适配器模式也是众多设计模式中的一种。
7、多态
其实将父类的对象应用于子类的特征就是多态。多态的实现并不依赖于具体类,而是依赖于抽象类和接口。多态是同一个行为具有多个不同表现形式或形态的能力。在多态的机制中,父类通常会被定义为抽象类,抽象类中给出一个方法的标准,而不给出具体实现。下面通过一个例子来理。
7.1、多态示例
定义一个Person类:
public class Person {
//这个方法的参数为Food类型
public void eat(Food food) {
food.taste();
}
}
定义一个抽象类Food:
/*
* 食物类定义为抽象类
* 因为不确定具体是哪一种
*/
public abstract class Food {
//定义一个抽象方法
public abstract void taste();
}
定义Food类的3个子类:Meat类、Flour类、Rice类:
public class Meat extends Food{
@Override
public void taste() {
System.out.println("补充大量脂肪和卡路里!");
}
}
public class Flour extends Food{
@Override
public void taste() {
System.out.println("兰州拉面就是牛!");
}
}
public class Rice extends Food {
@Override
public void taste() {
System.out.println("江南一带以米饭为主食!");
}
}
测试:
public class Test {
public static void main(String[] args) {
Person p = new Person();
Food meat = new Meat();
Food flour = new Flour();
Food rice = new Rice();
// 参数传入子类Meat类型
p.eat(meat);
// 参数传入子类Flour类型
p.eat(flour);
// 参数传入子类Rice类型
p.eat(rice);
}
}
执行,控制台:
调用的是同一个方法eat,但是传入的参数不同,执行的结果也不同,这就是多态的体现。
7.2、接口
其实在多态的机制中,有比定义抽象类更加方便的方式,那就是将抽象类定义为接口,由抽象方法组成的集合就是接口,也就是说,接口中只能有抽象方法。
因为Java并不支持多态继承,所有子类只能有一个直接父类,Java允许一个类同时实现多个接口,接口用来被实现。
接口的定义关键字是interface,如下:
public interface A{
public void a();
public void b();
public void c();
}
以下通过一个例子来说明。
定义一个接口Computer:
/*
* 定义接口
*/
public interface Computer {
// 接口中定义公共的静态常量
public static final int MAX_NUM = 100;
// 接口中定义的方法只能为抽象方法
double count(double a, double b, int tag);
}
定义接口的实现类ComputerImpl:
/*
* 接口的实现类,必须实现接口中的所有抽象方法
*/
public class ComputerImpl implements Computer {
@Override
public double count(double a, double b, int tag) {
if (tag == 0) {
return a + b;
} else if (tag == 1) {
return a - b;
} else {
return a * b;
}
}
}
测试:
public class Test {
public static void main(String[] args) {
Computer com = new ComputerImpl();
System.out.println("和值为:" + com.count(2.5, 4, 0));
System.out.println("差值为:" + com.count(2.5, 4, 1));
System.out.println("乘积值为:" + com.count(2.5, 4, 2));
}
}
执行,控制台如下:
接口也可以继承,说明如下:
- 接口只能由接口继承。
- 子接口继承的目的是拥有父类接口功能的基础上添加新定义。
- 非抽象类实现接口必须实现所有接口方法。
以下用代码说明。先定义一个接口:
public interface CountManager {
double count(double a, double b, int tag);
// 该接口计算梯形面积
double getArea(double up, double down, double h);
// 该接口获取最大值或最小值
double getMaxOrMin(double a, double b);
}
然后定义一个子接口继承以上接口:
/*
* 子接口,继承CountManager接口
*/
public interface ValidateManager extends CountManager {
// 定常量
boolean YES = true;
boolean NO = false;
int MAX_AGE = 18;
String LOGIN_NAME = "ycz";
String LOGIN_PWD = "ycz111111";
int MIN_LENGTH = 6;
int MAX_LENGTH = 9;// 定义常量
// 该接口验证年龄
boolean checkAge(int age);
// 该接口验证登录
String validatePass(String username, String password);
}
定义接口的实现类:
/*
* 接口的实现类
*/
public class ValidateManagerImpl implements ValidateManager {
@Override
public double count(double a, double b, int tag) {
if (tag == 0) {
return a + b;
} else {
return a - b;
}
}
@Override
public double getArea(double up, double down, double h) {
double area = (up + down) * h / 2;
return area;
}
@Override
public double getMaxOrMin(double a, double b) {
return Math.max(a, b);
}
@Override
public boolean checkAge(int age) {
if (age >= ValidateManager.MAX_AGE) {
return ValidateManager.YES;
}
return ValidateManager.NO;
}
@Override
public String validatePass(String username, String password) {
if (password.length() < ValidateManager.MIN_LENGTH ||
password.length() > ValidateManager.MAX_LENGTH) {
System.out.println("密码长度不对!");
}
if (username.equals(ValidateManager.LOGIN_NAME) &&
password.equals(ValidateManager.LOGIN_PWD)) {
return "验证通过!";
} else {
return "用户名或密码错误!";
}
}
}
测试:
public class Test {
public static void main(String[] args) {
ValidateManager vm = new ValidateManagerImpl();
System.out.println(vm.count(2.5, 4, 1));
System.out.println("梯形面积是:" + vm.getArea(2.5, 5, 4));
System.out.println("较大值是:" + vm.getMaxOrMin(4.6, 12.7));
System.out.println("成年了吗?" + vm.checkAge(25));
System.out.println(vm.validatePass("ycz", "ycz11"));
System.out.println(vm.validatePass("ycz1", "ycz111111"));
System.out.println(vm.validatePass("ycz", "ycz111111"));
}
}
执行,控制台如下:
7.3、关于向上转型
上转型可以理解的简单一点,就是类型是父类,但是对象指向子类的实例。
以代码说明:
public class A {
public void mess(String mess) {
System.out.println("AAAAA" + mess);
}
}
public class B extends A {
@Override
public void mess(String mess) {
System.out.println("BBBBB" + mess);
}
}
public class Test {
public static void main(String[] args) {
// a是A类型,指向B的实例
A a = new B();
a.mess("你好!");
}
}
执行,控制台:
上面的a对象就是一个上转型对象。上转型对象是没有问题的,范围由小到大,子类的实例一定是父类类型的对象。但是父类的实例不一定是子类类型的对象,这个是范围由大大小,因此需要判断,也就涉及到向下转型。
7.4、关于向下转型
向下转型是强制将父类对象转换为子类实例,这是有问题的,因为父类对象不一定是子类的实例,这种大范围到小范围有问题,因此,在强制转换前,必须要判断,用instanceof操作符判断。下面以例子说明:
A类:
public class A {
public void mess(String mess) {
System.out.println("AAAAA" + mess);
}
}
B类:
public class B extends A {
@Override
public void mess(String mess) {
System.out.println("BBBBB" + mess);
}
}
C类:
public class C extends A {
@Override
public void mess(String mess) {
System.out.println("CCCCCC" + mess);
}
}
测试:
public class Test {
public static void main(String[] args) {
A a = new B();
// 将A类对象a转换为B类型
// 转换前先判断
if (a instanceof B) {
// 强制转换成子类型,即向下转型
B b = (B) a;
b.mess("2021");
} else if (a instanceof C) {
C c = (C) a;
c.mess("2021");
}
}
}
执行,控制台:
强制转换前,使用instanceof操作符判断是个好习惯。
8、面向对象编程的四大特征
面向对象编程:Object Oriented Programming,简称为OOP。
面向对象有四大特征:封装、继承、抽象、多态。这也是面试中问的比较多的一个问题,也是比较重要的。回答的时候先总体概述,再分部阐述。
8.1、封装
封装是面向对象编程的核心思想。即将对象的属性和方法封装到类中,类就是载体。类通常对用户隐藏其细节,采用封装的思想保证了内部数据结构的完整性,使用该类的用户不能直接操作此类的数据结构,只能操作类允许的公开数据和方法,避免了外部操作对内部数据的影响,保证了安全性,提高了程序的可维护性。
用比较官方的话就是:将对象封装成一个高度自治和封闭的个体,对象的状态由这个对象自己的行为来读取和改变,即封装是隐藏一切可以隐藏的东西,只对外部提供接口,隐藏了内部特性,保证了内部的安全,比如一个人,要有自己的set方法来设置属性,有自己的get方法来获取属性。
8.2、继承
继承是父类的扩展,即子类延续了父类的属性和行为。也可以在此基础上添加新的属性和行为,可根据需要覆盖父类中的方法,继承提高了某些代码的复用性。
8.3、抽象
比如类,将一类对象的共同特征总结出来构造类的过程,产生了类。在这个类中,只考虑相似之处,忽略一些无关的东西,即只关注对象的哪些行为和属性,而不关注它们是什么具体的实现,现实中拥有共同特征的对象集合,可以将它们抽象为一个具有共同属性和行为的类。
8.4、多态
多态是指允许相同类型或不同类的子类型对象对同一消息作出不同的响应。如方法重载和方法重写都多态的体现。狭义上的多态指声明一个父类型的引用指向具体子类型的实例,在运行期间才能绑定,即运行时多态。