继承让人们更容易实现对已有的类的扩展。继承有两个主要的作用:1.代码复用,更容易实现类的扩展;2.便于对事务建模。
对于继承我们从一张图开始了解
这个是生物的部分分类,树模型。假设我们定义了动物类,那么其下还有很多不同的分类,那么我们在定义比如说哺乳动物类时,只需要扩展动物类,相同的,我们要定义猿类,只需要对哺乳动物类进行扩展,这就是继承,子类就是父类的扩展。根据上图即有哺乳动物类继承了动物类,猿类继承了哺乳动物类,而且相应的,子类有着父类的属性和方法。下面给出具体的实现:
public class Study {
public static void main(String[] args) {
Ape a = new Ape();
a.name = "猿猿";
a.weight = 60;
a.height = 1.3;
a.characteristic = "温顺";
a.favorite_food = "香蕉";
a.tell();
a.tell_();
a.intro();
}
}
class Animals{
String name;
double weight;
double height;
public void tell(){
System.out.println("Animals!");
}
}
class Mammal extends Animals{
String characteristic;
public void tell_(){
System.out.println("Mammal!");
}
public Mammal(){
}
public Mammal(String name, int weight, int height){
//子类拥有父类的属性
this.name = name;
this.weight = weight;
this.height = height;
}
}
class Ape extends Mammal{
String favorite_food;
public void intro(){
System.out.println("这个猿猴叫"+name+",重"+weight+"千克,高"+height+"米,性格"+characteristic+",喜欢的食物是"+favorite_food);
}
}
- 父类同时也被称作超类、基类、派生类等等;
- Java中的继承是单继承,不能多继承(接口有多继承),可以通过单继承实现多层继承(继承体系)。这个点会在下面给出例子。
- 子类继承父类可以获得父类全部的属性和方法(父类构造器除外),但不一定可以直接访问(关于封装会在后面提到)。
- 如果定义一个类是,没调用extends,则它的父类默认为java.lang.Obeject。(Object类在后面会提到)
关于单继承、多继承、多层继承给出下面的代码例子
public class Study{
public static void main(String[] args) {
C c = new C();
c.a = 1;c.b = 2;c.c = 3;
c.tell();c.tell_b();c.tell_c();
}
}
class A{
int a;
public void tell(){
System.out.println(a);
}
}
//B继承A
class B extends A{
int b;
public void tell_b(){
System.out.println(b);
}
}
//这里就是一个多层继承了,C继承B,而B又继承A,形成了一个继承体系
class C extends B{
int c;
public void tell_c(){
System.out.println(c);
}
}
//注意D中有与A中同名的方法
class D{
int d;
public void tell(){
System.out.println(d);
}
}
/*如果采用了下面的这种继承,则不行,会报错
因为如果采用了多重继承,那么如果使用了tell方法
就会造成混乱,使得继承复杂,系统难以维护
class E extends A,D{}*/
instanceof运算符在前面列出来过,他是一个二元运算符,左边是对象,右边是类;当对象是右边的类或者子类的对象时返回true,否则返回false。我们用上面的代码进行测试,main函数中添加代码如下
System.out.println(c instanceof B);
System.out.println(c instanceof A);
方法的重写需要符合下面三个点(注意要和方法的重载区分)
- 方法名、形参列表相同
- 返回值类型和声明异常类型,子类小于等于父类
- 访问权限,子类大于等于父类
具体实现代码
public class Study{
public static void main(String[] args) {
C c = new C();
c.a = 1;c.b = 2;c.c = 3;
c.tell();//调用类C中的tell方法,已经被重写了,就执行重写后的代码,不再执行类A或类B中的tell方法
}
}
class A{
int a;
public void tell(){
System.out.println(a);
}
}
class B extends A{
int b;
public void tell(){
System.out.println(a+b);
}
}
class C extends B{
int c;
public void tell(){
System.out.println(a+b+c);
}
}
结果如下
final关键作用:
- 修饰变量:被他修饰的变量不可改变。一旦赋予初值不能被重新赋值。
final int A = 1;
- 修饰方法:该方法不可被子类重写,但是可以被重载。
final void func(){}
- 修饰类:修饰的类不可被继承。
Final class A{}
首先要明白一点我们前面所学习、定义以及后面定义的所有类都是Object类的子类,都具备Object类的特性。
Object类是所有Java类的根基类,也就是说所有Java对象都拥有Object类的属性和方法。若类声明中没有用extends指向其父类,则默认欺父类为Object类。
//等价
public class A{
} == public class A extends Object{
}
Object类中定义有public String toString()方法,返回值是String类型,源码如下
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
即默认返回“类名+@+十六进制的hashcode”,在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。实例和重写toString()如下
class A{
String name;
//@Override
public String toString() {
return "@" + name;
}
}
public class Study{
public static void main(String[] args) {
A a = new A();
a.name = "Java";
System.out.println(a);//toString()重写返回的字符串
Study s = new Study();
System.out.println(s);//toString()未被重写,返回默认的字符串格式
}
}
关于equals方法这里和==来做比较。
在Object类中定义有public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。该方法默认比较对象的hashCode,是同一个对象的引用返回true,否则返回false。当然这个方法也可以被重写。具体例子如下
class A{
int id;
}
class B{
int id;
public B(int id){
this.id = id;
}
@Override
//只要id对的上返回true
public boolean equals(Object obj) {
if(obj == null)
return false;
else{
if(obj instanceof B){
B b = (B)obj;
if(b.id == this.id)
return true;
}
}
return false;
}
}
public class Study{
public static void main(String[] args) {
A a1 = new A();
A a2 = a1;
System.out.println(a1 == a2);
System.out.println(a1.equals(a2));
B b1 = new B(5);
B b2 = new B(5);
System.out.println(b1 == b2);//不是同一对象
System.out.println(b1.equals(b2));
/*
* JDK提供的一些类,比如String、Data等,重写了Object的equals方法*/
String s1 = new String("ABCD");
String s2 = new String("ABCD");
System.out.println(s1 == s2);//不是同一对象
System.out.println(s1.equals(s2));
}
}
super是直接父类对象的引用,可以通过super来访问父类中被子类覆盖的方法或者属性。super可在子类中随便调用。
Ps.若构造器第一行代码没有显式调用super(···)或者this(···),Java都会默认调用super(),含义是调用父类的无参数构造器。这里的super()可以省略。示例如下:
public class Study{
public static void main(String[] args) {
new B().func();
}
}
class A{
int num;
public void func(){
num = 1;
System.out.println("A类值="+num);
}
}
class B extends A{
int num;
public void func(){
super.func();//调用父类的方法
num = 2;
System.out.println("B类值="+num);
System.out.println("子类值="+num);
System.out.println("父类值="+super.num);//调用父类的属性
}
}
1.属性/方法查找顺序(假设查找变量a)
1 在当前类中查找属性a
2 依次上溯每个父类,查看每个父类中是否有a,知道Object为止
3 如果没有找到,则出现编译错误
4 只要找到变量h就终止这个过程
2.构造器调用顺序
构造器的第一句总是super(···),用来调用与父类对应的构造器。故先向上追溯到Object,然后再依次向下执行类的初始化块和构造器,直到当前子类为止。
Ps.上一个博客里面提到了静态初始化块,其实静态初始化块调用顺序和构造器调用顺序是一样的。
下面给出示例:
class A{
public A(){
System.out.println("First--A");
}
}
class B extends A{
static int num;
public B(){
System.out.println("Second--B");
}
public static void print(int num){
System.out.println(num);
}
static {
num = 1;
print(num);
}
}
class C extends B{
static int num;
public C(){
System.out.println("Third--C");
}
static {
num = 2;
print(num);
}
}
public class Study{
public static void main(String[] args) {
new C();
}
}
上面也用到了静态初始化块,也可以由输出得知静态初始化块的优先级是高于构造器的,输出如下
封装(encapsulation)是面向对象编程的三大特征之一,是最容易去理解的一个。
什么是封装?先从一个例子开始说起,我们日常生活总编程总需要用的笔记本(台式电脑),在正常情况下,我们只需要去开机、使用、关机,所以笔记本的内部结构我们是不会动的,因此在出厂时就是把内部的结构用外壳给封装起来,只留给用户需要用的就行,比如显示屏、usb接口、电源接口等等。这就是一种典型的封装了,不需要让用户知道的隐藏起来,而用户需要的就公开。封装就是把对象的属性和方法结合为一个独立的整体,对可信的类或者对象使用内部的内部属性和方法,对不可信的类或者对象进行隐藏
封装的优点如下
- 提高代码的安全性和复用性
- 封装细节,便于修改内部代码,提高可维护性。(高内聚)
- 简化外部调用,便于调用者使用,便于扩展和协作。(低耦合)
封装的实现需要利用访问控制符,访问控制符有四种,分别为private、default、protected、public,它们说明了面向对象的封装性。访问权限如下:
- public表示公开,可以被该项目的所有包中的所有类访问
- protected表示可以被同一个包中的类以及其他包中的子类访问
- default表示没有修饰符修饰,只能访问同一个包的类
- private表示私有,只有自己的类可以访问
接下来给出四个类First、Second、Third、Fourth,其中前两个类是一个包,后两个另一个包,举出例子:
public
package yang.blog1;
public class First {
//编译通过->同一个类中可以访问
public String ch;
public int num;
@Override
public String toString() {
return "ch="+ch+",num="+num;
}
public static void main(String[] args) {
First f = new First();
System.out.println(f);
}
}
package yang.blog1;
public class Second {
//编译通过说明同一个包中可以访问
public static void main(String[] args) {
First f = new First();
System.out.println(f.ch + f.num);
}
}
package yang.blog2;
import yang.blog1.First;
public class Third {
//编译通过,说明不同包中可以访问
public static void main(String[] args) {
First f = new First();
System.out.println(f.name + f.num);
}
}
package yang.blog2;
import yang.blog1.First;
public class Fourth extends First {
//编译通过,说明不同包中的子类可以访问
public static void main(String[] args) {
First f = new First();
System.out.println(f.ch + f.num);
}
}
编译都没有出现错误,说明同一类,同一包,不同包中的子类,不同包都可以访问。
protected
将First的两个属性的访问权限修饰符改为protected,其他类中不做修改。
public String ch;
public int num;
发现Third类出现了编译错误,说明了同一类,同一包和不同包中的子类都可以访问。
default
同理我们将protected改为default(default不写)
String ch;
int num;
可以发现后面两个类出现了编译错误,说明同一类,同一包中可以访问。
private
同理访问权限修饰符改为private。
private String ch;
private int num;
可以发现除了第一个类未出现编译错误,其他的都出现了,说明只有在同一个类中才能访问。
封装的使用细节
类的属性的处理
- 一般使用private访问权限
- 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头的)
- 一些只用于本类的辅助性放大可以用private修饰,希望其他类调用的方法用public修饰
给出示例:
public class Study {
public static void main(String[] args) {
A a = new A();
a.setName("NAME");
a.setFlag(true);
System.out.println(a.getName()+" "+a.getFlag());
}
}
class A{
private String name;
private boolean flag;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public boolean getFlag(){
return flag;
}
public void setFlag(boolean flag){
this.flag = flag;
}
}
结果如图
多态是指同一个方法由于由于调用的对象的不同可能会产生不同的行为。比如用笔记本,他用来看视频,你拿来查阅资料,我用来编程,同一个用电脑的方法却出现了多种行为,这就是多态。
关于多态,要注意一下三个点
- 多态是方法的多态,而不是属性的多态(多态与属性无关)
- 多态的存在需要有三个必要条件:继承,方法重写,父类引用 指向子类对象
- 父类引用指向子类对象后,用该父类引用调用子类的重写方法,此时多态就出现了
关于父类引用指向子类对象的理解,需要知道对象的转型,对象的转型在下面有写,这里我们先简单的弄出多态是个什么东西。给出下面的示例:
class Person{
public void Using_PC(){
System.out.println("拿电脑");
}
}
class He_She extends Person{
public void Using_PC(){
System.out.println("看视频");
}
}
class You extends Person{
public void Using_PC(){
System.out.println("查资料");
}
}
class Me extends Person{
public void Using_PC(){
System.out.println("编程");
}
public void Other(){
System.out.println("玩Terraria");
}
}
public class Study {
public static void main(String[] args) {
Person p1 = new He_She();//向上自动转型(对象的转换)
//指向的是哪一个类就调用哪个类的方法
p1.Using_PC();
Person p2 = new You();//向上自动转型
p2.Using_PC();
Person p3 = new Me();//向上自动转型
p3.Using_PC();
//要注意的是p1、p2、p3都是编译类型,它们所指向的对象才是运行时的类型
//有了多态,只需要让增加的这个类继承Person类就行了
//如果没有多态我们需要些很多的重载方法,很麻烦
//若要调用运行时类型的方法(该方法不满足多态),只能进行强制类型转换,否则会编译错误
//p3.Other();这个就是错误的
Me me = (Me)p3;//向下强制类型转换
me.Other();//要这样才是正确的
//或者这样写 ((Me)p3).Other();
}
}
结果如下
多态提高了代码的扩展性,但是也有弊端,就是不能调用子类的特有的方法。
我们再来说说父类引用指向子类对象,我觉得上面的例子已经很好地写出来了,父类引用变量p1、p2、p3,而p1、p2、p3分别指向的是子类He_She、You、Me。(如果有人看到这里,有什么好的意见,可以和我交流交流XD)
(终于到最后一个点了T.T,记录还是蛮辛苦的)
对象的转型分为向上转型和向下转型,上面的代码的注释部分已经有过提示了。
父类引用指向子类对象,这个过程称为向上转型,属于自动类型转换。向上转型后的父类引用变量只能调用它编译类型的方法,而不能调用它运行时类型的方法(这里在上面的代码中已经提到过了,可以参考上面的代码),这时需要进行类型的强制转换,称作向下转型。
要注意的是在向下转型时,必须将引用变量转换为运行类型,否则将会出现类型转换异常,导致编译错误。(避免这种错误可以使用if分支和instanceof运算符->这是书上写的,我怎么感觉没什么必要…)