今天乘着还有一些时间,把上次拖欠的面向对象编程三大特性中遗留的继承和多态给简单说明一下。这一部分还是非常重要的,需要仔细思考。
继承
继承:它是一种类与类之间的关系,通过使用已存在的类作为基础来建立新类。其中已存在的类称为父类(或基类); 建立的新类称为子类(或派生类)。简单的就是子类继承父类的非私有属性和方法。
需要注意的是,新定义的类可以选择继续使用父类的功能或者自己增加新的数据或新的功能,但不能选择性地继承父类。(要么继承所有(前提是非私有),要么就不继承)
只要能满足 "A is a B"的关系就可以形成继承关系,代码中是通过 extends 关键字来实现继承的。
特别注意:在java中只能继承一个父类(也就是单继承),而且子类可以访问父类的非私有成员。这个和Python不一样,Python的继承可就灵活了。
我们知道子类继承了父类之后,可以访问父类的非私有成员;但是父类的私有成员,子类还是无法直接访问。如果我们想访问呢?可以通过父类暴露的公有方法来实现间接访问。
父类对象不可以访问到子类特有的方法或属性,同时父类不可以访问子类特有成员(那怕是公有的成员)
重载
方法重载必须同时满足以下条件:
- 同一个类中;;
- 方法名相同,参数列表不同(参数顺序、个数、类型);
- 方法返回值、访问修饰符任意;
- 与方法的参数名无关。
public void printinfo() {
System.out.println("方法重载1");
};
public void printinfo(String name) {
System.out.println("方法重载2");
};
public String printinfo(String name, int age) {
return "方法重载3";
};
public String printinfo(String age, String name) {
return "方法重载4";
};
public String printinfo(int age, String name) {
return "方法重载5";
};
// 与方法的参数名无关,加上下面的代码会和上面的 printinfo(int age, String name)造成重复而报错:
public String printinfo(int size, String name) {
return "方法重载5";
};
重写
方法重写也必须同时满足以下条件:
1、在满足继承关系的子类中;
2、方法名相同,参数列表相同(参数顺序、个数、类型);
3、方法返回值相同或者是子类类型(但不允许是Object类型,可以向下兼容,向上是不可以的);
4、访问修饰符的限定范围大于等于父类方法。
(一大两小,子类的访问修饰符的限定范围大于等于父类;子类的返回值类型和异常都需要小于等于父类)
注意:在子类中是可以定义与父类重名的属性的,但这并不说明属性是可以重写的。
访问修饰符
在Java里面一共包含4种访问修饰符,分别是:
1、private:私有的;
2、默认;
3、protected:受保护的;
4、public:公共的。
其中,private:只允许在本类范围中进行访问,离开了当前类就不允许访问;
默认: 允许在当前类,同包子类/非子类都可调用,跨包子类/非子类都不允许;
protected:允许在当前类,同包中的子类/非子类都可以以及跨包子类调用。跨包的非子类不允许调用。
public:允许在任意位置访问。
按照前面的顺序,自上而下,访问范围越来越大;自下而上,限制能力越来越强:
(同包包括同包子类与非子类;子类包括同包子类和跨包子类)
访问修饰符对方法重写的影响
子类重写父类方法时,访问修饰符是允许改变的,要求是: 子类的访问范围必须大于等于父类的访问范围。也就是说如果父类访问修饰符是public,那么子类的访问修饰符也必须是public,其他的类似。
继承的初始化顺序
继承后的初始化顺序如下:
父类静态成员 -> 子类静态成员 -> 父类对象的构造 -> 子类对象的构造
(父类静态成员 -> 子类静态成员 -> 父类对象的构造->父类的构造方法 -> 子类对象的构造->子类的构造方法)
一个问题: 访问修饰符影响成员加载顺序?静态成员优先于静态代码块执行?
访问修饰符不影响成员加载顺序,跟书写位置有关。如果把静态代码块写在静态变量的前面,那么先执行静态代码块。
super关键字
如果子类继承并重写了父类的方法,那么我们通常调用的就是重写后的子类方法。如果需要调用父类的方法,我们可以使用super.方法
来达到这个目的。
当然也可以使用super.属性
来达到访问父类的非私有属性的目的。
尽管父类的构造方法的访问修饰符是public,但是它却不可以被子类继承和重写的。
虽然它两个不可以,但是它的存在却是非常必要的,因为子类对象的实例化要依赖于父类对象的构造方法(默认,无参或有参的构造方法)。
如果子类调用了自己有参的构造方法,而父类定义了有参和无参的构造方法,程序依然是调用父类无参的构造方法。也就是说,我们在子类的构造方法中没有显式标注的情况下,默认调用父类的无参构造方法,因此父类的无参构造方法很重要,一定要写,否则会影响子类的对象实例化。
如果子类构造方法中既没有显式标注,且父类中没有无参的构造方法,则引发编译错误。
我们可以使用super(参数)
这种形式来调用父类允许被访问的其他构造方法,但是此时super()必须放在子类构造方法有效代码的第一行(必须是子类的构造方法(其他方法不行)的第一行(其他行不行))。
也就是说父类在实例化的时候会默认调用无参的构造方法(此时你不定义无参的构造方法是可以的),但是如果子类在实例化对象的时候没有显示标志(也就是会默认调用父类无参的构造方法),而此时父类其实是不存在无参的构造方法,所以会引发编译错误。
this和super的对比
this:当前类对象的引用:
1、访问当前类的成员方法;
2、访问当前类的成员属性;
3、访问当前类的构造方法;
4、不能在静态方法中使用;
super:父类对象的引用:
1、访问父类的成员方法;
2、访问父类的成员属性;
3、访问父类的构造方法;
4、不能在静态方法中使用;
注意:在调用构造方法时,this和super不能同时存在(前面说过两者都要求在第一行)。
Object类
Object类是所有类的父类,这个其实和Python中差不多,在Python里面也是所有的类都继承于object这个基类。点这直接查看api:javase8api
一个类没有使用extends关键字明确标识继承关系,则默认继承Object类(包括数组)。
Class Object is the root of the class hierarchy.
Every class has Object as a superclass.
All objects, including arrays, implement the methods of this class.
Object类存放于java.lang包中,这个包系统默认会为我们直接加载。
equals用法
如果子类没有重写Object类的equals方法,那么比较的是两个引用是否指向同一个地址;而String类则重写了Object类的equals方法,所以比较的是字符串的值是否相等。(言外之意,子类可以通过重写equals方法的形式,改变比较的内容)
因此我们不能这样说equals比较的两个对象的值,或者引用地址,但是我们却可以说"=="比较的却一定是两个对象的引用地址。
toString用法
api告诉我们,toString最后返回的是下面这种形式:(包名.类名@内存中的哈希码)
getClass().getName() + '@' + Integer.toHexString(hashCode())
同样的,子类如果没有重写Object类的toString方法,那么则会打印输出其在内存中的哈希码;而String类则重写了Object类的toString方法,所以打印输出其真实值。(言外之意,子类可以通过重写toString方法的形式,改变输出的内容)
还要说明的一点就是输出对象
和对象.toString
的效果是一样的,因为直接输出对象的时候其实是调用了对象.toString方法。
Final关键字
当我们不希望某些类被继承,某些方法被重写或者某些数据被修改时,可以使用final关键字来实现这个目的。
如果某个类被final修饰,则表明该类不可以被继承,该类没有子类,public final class/final public class都可以,只要是放在class的前面就可以;
如果某个方法被final修饰,则表明该方法不可以被重写,但是并不影响子类去继承调用它(final不可以修饰构造方法)。
如果某个局部变量被final修饰,那么我们可以不用在声明的同时立马进行赋值,但是必须在使用之前进行赋值,一旦赋值就不能被修改;
(方法内的局部变量的作用范围,从该行开始到所在大括号结束;而类的成员变量的作用范围取决于它前面的访问修饰符);
如果某个成员变量被final修饰,我们同样不需要声明的同时进行立马赋值,但是必须在使用之前进行赋值,而且只能在构造方法或者类代码块(构造代码块)中进行赋值,一旦赋值就不能被修改;也就是说类中成员属性的赋值可以有三种方式: 1. 定义是直接初始化; 2. 构造方法; 3. 构造代码块(类代码块)。
注意: 当具有多个构造方法时,final关键字修饰的成员变量如果选择了在构造方法里面进行赋值,那么就需要在所有的构造方法里面进行赋值,但是不同构造方法是可以赋不同值的
final对数据类型的影响
我们知道java 数据类型分为基本数据类型(byte,short,int,long,float,double,char,boolean) 和 引用数据类型(array,String,interface,自定义的类...)
基本数据类型是可以直接进行赋值的,而引用类型需要实例化该类的对象,然后才能给其对象进行赋值(String这个比较特殊,两种形式都是可以的)
我们知道基本数据类型在内存中存放的是数据本身,而引用数据类型在内存中存放的则是对象的引用地址。
下面的例子告诉我们,被final修饰的对象不可以修改它的引用地址,但是属性却是可以的:
final Test test=new Test("hello");
// test=new Test ();
Test.key="world";
总结一下就是:基本数据类型的变量其一旦被赋初值,就无法进行修改;而引用类型的变量只是在初始化之后不能再指向另一个对象,但是对象的内容却是可变的。
因此final可配合static使用,用来修饰方法和变量。通常是用于修饰配置信息等(因为这类信息只需要加载一次,而且后面不需要被修改)。言外之意,使用final修饰可以提高性能,但会降低可扩展性。
普通代码块,类代码块,构造代码块,静态代码块区别
代码块都是一对大括号{}所括起来的内容。
普通代码块就是一对大括号{}所括起来的内容,只存在于类的方法之中;
类代码块和构造代码块是一个东西,就是直接在类中进行定义的,前面没有static进行修饰。构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类的构造方法。
静态代码块前面有static关键字进行修饰,它不能存在于任何方法体内,也不能直接访问实例变量和实例方法,需要通过类的实例对象来访问。
通常我们new一个对象,JVM要经过这样的初始化顺序:父类静态块>子类静态块>父类属性>父类构造器>子类属性>子类构造器,这一系列的工作会消耗大量的内存和cpu。
具体的研究可以参看这里:详解java中的四种代码块。
java中的注解
注解是JDK1.5版本引入的一个特性, 它可以声明在包、类、属性、方法、局部变量、方法参数等前面,作用就是对这些元素进行说明、注释。
按照运行机制来分类
注解按照运行机制来进行划分,可以分为3部分:源码注解,编译时注解,运行时注解。
源码注解:只在源码.java文件中存在,编译成.class字节码文件就不存在了;
编译时注解:在源码.java文件和字节码.class文件中都存在;
运行时注解:在运行阶段还起作用,甚至会影响运行逻辑的注解。(spring框架中的@Autowired依赖注入的这种注解,它实现的就是在程序运行的过程当中自动的将外部传入的信息加载进去,它就是一种可以影响程序运行逻辑的运行时注解。)
按照来源来分
注解按照来源来进行划分,可以分为3部分:JDK注解,第三方注解,自定义注解。
还有一种元注解,它是对注解进行注解的。
不行了,写着写着字数又超了,快4000字了,面向对象最后一个特性:多态,我还没说呢,下次吧,今天得滚去运动了。。。感谢你的赏阅。