继承概述:
把多个类中相同的内容提取出来定义到同一个类中的方法叫做继承,在java中用关键字extends来表示继承。
格式是:class 子类名 extends 父类名{}。
好处:
1.提高了代码的复用性。
2.提高了代码的可维护性。
3.让类与类之间产生了关系,是多态的前提。
缺点:
1.类与类之间产生了关系其实也是一个弊端,类的耦合性增强了。而开发有一个重要原则叫:低耦合,高内聚,耦合是指类与类的关系,内聚是指自己完成某件事情的能力。
2.打破了封装性。
java中继承的特点和注意事项
1.java只支持单继承,不支持多继承。就是说一个子类只能有一个父类,但一个父类可以有很多个子类,同时父类还可以再有父类,这种多层继承方式形成了java的继承体系。
在某些语言中是支持多继承的,格式:extends 类1,类2,……..
2.子类只能继承父类的所有非私有(private)成员(包括成员方法和成员变量)。这里其实体现出了继承的另一个弊端,打破了封装性,虽然子类不能继承父类私有的成员变量,但是父类中使用了私有的成员变量的非私有成员方法可以被子类继承,这样的话子类其实可以使用父类私有的成员变量,打破了封装性。
class Father{
public int num = 10;
private int num2 = 20;
public void method(){
System.out.println(num);
System.out.println(num2);
}
private void show(){
System.out.println(num);
System.out.println(num2);
}
}
class Son extends Father{
public void function(){
System.out.println(num);
/*System.out.println(num2);
错误: num2可以在Father中访问private 子类不能继承父类的私有变量 同理s.show也是报错的,不能继承父类的成员方法
System.out.println(num2);
^*/
}
}
class ExtendsDemo2{
public static void main(String[] args){
Son s = new Son();
s.method();/*通过访问父类的非私有成员方法,由于父类的成员方法使用了父类中私有的成员变量,我们相当于间接引用了父类的私有的成员变量
这里按照我的理解就是打破了封装性,原本无法在其他类中访问的私有变量变得能够访问*/
s.function();
}
}
3.子类不能继承父类的构造方法,但是可以通过super关键字去访问父类的构造方法。
4.不能为了继承而使用继承,判断使用继承的可以用“is a”的方法。
比如:水果
苹果,香蕉和白菜
苹果和香蕉is a 水果,而白菜不是,虽然都具有一些共同的特质,但不宜使用继承关系。
继承中的成员变量,构造方法,成员方法和关键字this,super
ONE. 继承中的成员变量
1.子类中某一成员变量名称和父类中任何一个成员变量都不一样。
这种情况下子类正常继承父类中的不同名变量。
2.子类中某一成员变量名称和父类中某一成员变量的名称一样。
这种情况下,子类中确定一个变量的顺序是:就近原则
A:先在子类的局部范围内找,有就确定。(这里找到的变量只是局部变量,由于局部变量运行完就在内存中消失,所以局部变量是不能用public来修饰的)。
B:如果没找到,再在子类的成员范围中找,有就确定。
C:如果还是还是没有,再到父类的成员范围中找,有就确定。
D:如果还是没有,最后报错。
3.this,super关键字调用成员变量。若我不仅仅要访问局部的同名变量,还要访问本类中的同名变量,还要访问父类同名的成员变量时,java中提供了了两个关键字this和super
格式是:this.成员变量;super.成员变量。
需要注意的是:this代表对本类成员变量的引用,super代表对对父类成员变量存储空间的标识。
区别是:
this.name:代表将本来的成员变量引用过来,你可以给这个变量赋值。
super.name:代表的是父类成员变量存储的空间,你可以理解成super.name是一个值,而不是变量,所以是不能给它赋值的。
class Fu{
private int age;
public Fu(int age){
this.age = age;
}
}
class Zi extends Fu{
public Zi(int age){
super(age);//这里我在自己编代码的时候犯了一个错误,就是super.name = name;这样是会报错name可以在Animal中访问private。
//所以这里不能直接给父类的name赋值,而是应该调用父类的构造放方法
}
}
class Father{
public int num = 10;
}
class Son extends Father{
public int num = 20;
public int num2 = 30;
public void show(){
int num = 40;/*ExtendsDemo4.java:9: 错误: 非法的表达式开始
public int num = 40;局部变量是不能够用public来修饰的
^*/
System.out.println(num);
System.out.println(this.num);
System.out.println(super.num);
System.out.println(num2);
}
}
class ExtendsDemo3{
public static void main(String[] args){
int num = 50;//在这里定义一个同名的变量虽然看上去很近,但其实是最远的,因为这个num最先被加载
Son s = new Son();
s.show();
}
}
TWO.继承中类的加载和初始化过程(继承中的构造方法)
谈到继承中的构造方法就不能不谈继承中类(包含测试类主类,子类,父类)的加载和初始化过程
1.类的加载和初始化过程(以主类调用子类构造方法为例)
第一步:进行类的加载。我们知道,java文件被java虚拟机加载后生成class文件,在这个编译的过程中,第一步就是要进行类的加载。类加载的顺序是主类——父类——子类,在类加载的同时,各类的静态代码块也会按照这个顺序依次进行。值得注意的是:无论这个类被调用几次,静态代码块只运行一次,其实这不难理解,我们可以将类的加载想象成激活过程,类只需要被加载一次,之后就可以随意调用。
第二步:运行主类main方法值得注意的是:在这个案例中,主类不要进行初始化过程,因为初始化过程只有在该类构造方法被调用的情况下运行。所以如果主类是通过类名调用了子类的静态方法,那么子类和父类不会进行初始化过程。
第三步:进行父类的初始化。主类调用的是子类的构造方法,为何要先对父类进行初始化过程呢?原因是子类继承了父类的数据,并有可能使用了父类的数据,所以在子类初始化之前,必须先对父类进行初始化。
初始化过程分为:
A.成员变量初始化
包含默认初始化int x;和显示初始化x=100;。成员变量知识点:int x = 100;成员变量是基本类型;Student s = new Student()成员变量是引用类型,同时这里也是一个创建对象的过程。
B.构造方法初始化
值得注意的是构造方法初始化之前,要先运行构造代码块,且每一次调用构造方法都要先运行构造代码块。
第四步:进行子类的初始化。子类的成员变量初始化,运行构造代码块,运行构造代码。
第五步:调用子类方法
class Fu{
static int age = 40;
static {
System.out.println("这是父类的静态代码块:"+age);
}
{
System.out.println("这是父类的构造代码块");
}
public Fu(){
System.out.println("这是父类的构造方法");
}
static void show(){
System.out.println("Fu show");
}
}
class Zi extends Fu{
static {
System.out.println("这是子类的静态代码块");
}
{
System.out.println("这是子类的构造代码块");
}
public Zi(){
System.out.println("这是子类的构造方法");
}
public void method(){
System.out.println("Zi method");
}
}
class TestDemo{
static {
System.out.println("这是主类的静态代码块");
}
{
System.out.println("这是主类的构造代码块");
}
public TestDemo(){
System.out.println("这是主类的构造方法");
}
public static void main(String[] args){
Zi z = new Zi();//这里是创建对象,同时也是调用Zi类构造方法。
z.method();
Zi.show();//这里是通过类名调用方法,所以不需要对类进行初始化过程
}
}
/*
输出结果
这是主类的静态代码块
这是父类的静态代码块:40
这是子类的静态代码块
这是父类的构造代码块
这是父类的构造方法
这是子类的构造代码块
这是子类的构造方法
Zi method
Fu show
*/
在这里对成员变量相关做一个总结
A.成员变量区除了对成员变量进行定义(并赋初始值之外),什么都不能做,将成员变量的定义和赋值分开也不行
class{
int x;
//x = 20;报错
}
B.无论是在初始化块,静态初始化块,构造器中定义的和成员变量同名的变量(注意代码块中定义的变量只能用默认或者final修饰),其编译不报错,但是不会对成员变量产生影响,代码块结束之后就消失在内存中
class Test{
{
//static int x = 0;public int x = 0;都会报错
//Illegal modifier for the variable x; only final is
// permitted
}
static{
//static int y = 0;public int y = 0;都会报错
}
Test(){
//static int z = 0;public int z= 0;都会报错
}
}
C.默认修饰的成员变量,可以在初始化块和构造器中初始化,不能在静态初始化块中初始化,可以赋值多次,也可以不赋值,会给出默认值
D.static修饰的成员变量,可以在初始化块,静态初始化块和构造器重初始化,可以赋值多次
E.final修饰的成员变量,可以在初始化块和构造器中初始化,不能在静态初始化块中初始化,和默认的成员变量不同的地方在于,只能赋值一次,且必须被赋值
F.static final修饰的成员变量,只能在静态代码块中初始化,不能再静态初始化块和构造器重初始化,且只能赋值一次,并且必须被赋值
package 随手打的测试类;
class Test1{
int a;
{
a = 10;
}
static{
//a = 10;报错无法在静态初始化块中初始化非静态的成员变量
int a = 10;//代码块中定义的变量,会消失在内存中,不对成员变量起作用
}
Test1(){
a = 20;//可以赋值多次,根据初始化顺序确定最后值
}
}
class Test2{
static int a;
{
a = 15;
final int a = 10; //代码块中定义的变量,会消失在内存中,不对成员变量起作用
}
static {
a = 10;
}
Test2(){
a = 20;
}
}
class Test3{
final int a;
{
a = 20;
}
static {
//a = 20;报错无法在静态初始化块中初始化非静态的成员变量
}
Test3(){
//a = 10;可以在构造器重赋值,但是final修饰的成员变量只能被赋值一次所以还是报错
}
}
class Test4{
static final int a;
{
//a = 10;
int a = 10;//加上int之后,表示的是这个代码块中初始化了一个变量a,代码块结束后就成了垃圾
//static int b =10;同时在代码块中,只有final和默认修饰符可以修饰代码块内部的初始化变量
}
static {
a = 20;
int a = 10;//这里同理,不会对成员变量a产生影响,代码块结束后消失在内存中
}
Test4(){
//a = 10;无法在构造器中初始化static final修饰的成员变量
}
}
public class TestDemo {
public static void main(String[] args) {
Test1 t1 = new Test1();
Test2 t2 = new Test2();
Test3 t3 = new Test3();
Test4 t4 = new Test4();
System.out.println(t1.a);//输出结果 20
System.out.println(t2.a);//输出结果 20
System.out.println(t3.a);//输出结果 20
System.out.println(t4.a);//输出结果 20
}
}
2.this,super关键字
A:子类的所有构造方法都会默认访问父类的空参构造方法
上面在类的初始化过程中我提到子类在进行初始化之前必须要先对父类进行数据初始化,这是如何做到的呢,其实java是通过隐藏了一段代码做到这一点的。这段隐藏的代码就是super();这段代码被隐藏在子类每一个构造方法的第一句,通过这段代码,子类来显示调用父类的空参构造方法。
B:当父类没有空参的构造方法时(没有构造方法的时候系统会默认给一个无参构造,但如果我们给出一个带参构造,系统就不会给出默认构造,此时也就没有无参构造),编译会报错,如何来解决这一问题呢?
有以下三种方法:
a:在父类中加一个无参构造。(编译不报错但没有实际意义)
b:通过super关键字去显示调用父类的带参构造。
c:通过this关键字去调用本类的其他构造方法。(这个构造方法一般通过super调用了父类的带参构造)
C:this,super关键字调用成员方法
格式:
this();调用本类的无参构造
super();调用父类的无参构造
this(参数);调用本类的带参构造
super(参数);调用本类的带参构造(可以理解成不需要创建对象或引用类名就可以从其他类调用构造方法)
必须注意的是:this(…),super(…)必须出现在每个构造方法的第一句否则的话,可能会出现父类多次初始化的现象。
class Father{
/*public Father(){
System.out.println("这是father的无参构造");
}*/
public Father(String name){
System.out.println("这是"+name+"的带参构造");
}
}
class Son extends Father{
public Son(){
//super();这里默认有一句super();当父类有无参构造时,相当于对父类数据进行了初始化
super("matty");//这里要加"",因为这里需要一个String型数据,不加的话相当于matty是一个未定义的变量名
System.out.println("这是son的无参构造");
}
public Son(String name){
//super();
this();//表示调用本类的无参构造
System.out.println("这是son的带参构造");
}
}
class ExtendsDemo4{
public static void main(String[] args){
Son s = new Son();
Son ss = new Son("傻子");
}
}
这是matty的带参构造
这是son的无参构造
这是matty的带参构造
这是son的无参构造
这是son的带参构造
3.类的初始化过程是分层初始化
在第二点我们提到,子类是通过super关键字来显示调用父类的构造方法的,而super(…)总是出现在子类构造方法的第一行,那么问题来了,从代码的角度,岂不是要对子类的成员方法初始化,运行子类的构造代码块,再去显示调用父类的构造方法?
从编译的结果来看,不是这样。理由是在java中子类和父类的初始化过程是分层初始化过程,即总是对父类先初始化,再对子类初始化,虽然代码出现在构造方法内,但是把整个初始化过程看作是一个整体。
class X{
Y b = new Y();
X(){//这里的构造方法可以不加public的原因也暂时不知道
System.out.print("X");
}
}
class Y{
Y(){
System.out.print("Y");
}
}
class Z extends X{
/*
原题这里是public class Z extends X,但是这样的话
面试题2.java:12: 错误: 类Z是公共的, 应在名为 Z.java 的文件中声明
public class Z extends X{
^
这里的原因应该是由于主类的名称和文件名称不同导致的,至于为什么会这样,暂时还不知道。
*/
Y y = new Y();
Z(){
System.out.print("Z");
}
public static void main(String[] args){
new Z();
}
}
/*
输出的结果是YXYZ
*/
/*
分析:
1:首先是主类的加载,这个案例没有静态代码块,所以运行main方法。main方法中new Z()要求运行本类的构造方法,而在这里主类z是继承x的,所以在子类加载构造方法之前要先对父类x进行初始化
2:对父类进行初始化,先进行成员变量初始话,Y b = new Y();这里创建对象引入Y类,对Y类进行初始化,运行Y类的构造方法。输出Y
3:然后运行父类的构造方法,输出X
4:回到本类,先运行成员变量初始化,输出Y,再运行构造方法输出Z。
*/
THREE.继承中成员方法和override方法重写
1.子类中成员方法和父类中成员方法声明(方法名和参数列表)不一样,那测试类用哪个方法就调用哪个方法。
值得注意的是对成员方法继承的理解:
继承中成员方法的理解,最开始我的理解是,子类继承父类,把父类可继承的东西放到子类来之后,就在子类中进行就可以了。
但是,这种理解是错误的,比如这里的父类中的成员变量是私有的,子类不能继承,但是又继承了其set,get方法,如果理解成就在子类中进行,是根本走不通的,所以,还是要回到最初老师的教法:
测试类中引用子类方法,子类没有看父类,父类没有报错。
放弃单纯的继承后割离开父子类的方法,把继承的两个类看作一个整体。
2.子类的成员方法和父类的成员方法声明一样,这种情况叫方法重写OVERRIDE这种情况下通过子类调用成员方法,总是调用子类中的方法。
注意事项:
A:父类中的私有方法不能被重写
原因是父类的私有方法子类根本不能继承
B:子类重写父类方法时,访问权限不能更低
最好就是一样的访问权限
C:父类如果是静态方法,那么子类也必须是静态方法才能重写
其实这个不算方法重写,但现象确实如此,至于为什么不算,后面多态会讲。
D:子类重写父类方法时候,最好访问权限一模一样
3.方法重写override和overload的区别
override方法重写:子类中出现了和父类声明一模一样的方法。
overload方法重载:本类中出现了方法名一样,但是参数列表不同的方法,与返回值无关。
4.this,super关键字调用成员方法
格式:super.方法名(参数)
class Phone{
public void call(String name){
System.out.println("给"+name+"打电话");
}
}
class NewPhone extends Phone{
public void call(String name){
super.call(name);
System.out.println("收听天气预报");
}
}
class ExtendsDemo5{
public static void main(String[] args){
NewPhone np = new NewPhone();
np.call("matty");
}
动物案例
class Animal{
private String name;
private int age;
private String color;
public Animal(){}
public Animal(String name,int age,String color){
this.name = name;
this.age = age;
this.color = color;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void setColor(String color){
this.color = color;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
public String getColor(){
return color;
}
public void eat(){
System.out.println("我要吃饭");
}
}
class Cat extends Animal{
public Cat(){}
public Cat(String name,int age,String color){
super(name,age,color);
}
public void play(){
System.out.println("猫在玩英雄联盟");
}
}
class Dog extends Animal{
public Dog(){}
public Dog(String name,int age,String color){
super(name,age,color);
}
public void look(){
System.out.println("狗在看猫玩英雄联盟");
}
}
class ExtendsDemo8{
public static void main(String[] args){
//test cat
Cat cat = new Cat("tom",5,"黑色");
System.out.println(cat.getName()+","+cat.getAge()+","+cat.getColor());
System.out.println("-------------------------");
cat.setName("hollokitty");
cat.setAge(8);
cat.setColor("白色");
System.out.println(cat.getName()+","+cat.getAge()+","+cat.getColor());
System.out.println("-------------------------");
cat.play();
System.out.println("-------------------------");
//test dog
Dog dog = new Dog();
dog.look();
}
}