第五章:面向对象上
面向对象的三大特征:
- 封装
- 继承
-
多态
Java提供了private、protected、public三个访问控制符来实现良好的封装,使用extends关键字让子类继承父类.
类和对象
定义类
概念:类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。
定义一个类时,一个类中包含了三种最常见的成员:构造器、成员变量(属性)和方法。其中成员变量用于定义该类或该类的实例所包含的状态数据,方法则用来定义该类或该类实例所具有的行为特征或功能,构造器用于构造该类实例。
定义成员变量名的规则:驼峰命名法,第一个单子首字母小写,后面的每个单词的首字母大写,其他字母小写。
构造器的返回值:在定义构造器时,不能定义返回值类型,也不能使用void声明没有返回值。当使用new调用构造器时,构造器返回该类实例,可以把该实例当做构造器的返回值。
**对象、引用:**Java在创建实例对象,并把实例对象赋值给变量时,仅仅是将实例对象的引用地址赋值给了变量p,这个地址指向该实例对象。在Java中,引用变量p存放在栈内存中,而实例对象存储在堆内存中。Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用去访问。堆内存中的对象可以有多个引用。
对象的this引用
this:this关键字总是指向调用该方法的对象。this的最大作用就是让该类中的一个方法访问该类中另一个方法或实例变量。大部分时候一个方法访问该类中其他方法或成员变量,加不加this效果一样,但是static修饰的方法不能使用this引用.
如果我们定义一个jump和run方法,在run中调用jump就可以使用this。
public void jump(){
System.out.println("jump");
}
public void run(){
this.jump();
System.out.println("jump and run");
}
方法详解
定义形参个数可变的方法:在定义方法时,在最后一个形参的类型后增加三个点(…),则表明该形参可以接受多个同类型的参数值,参数值被当做数组传入。
public class Test{
public static void test(int a, String ... books){
for(var temp:books){
System.out.println(temp);
}
System.out.println(a);
}
public static void main(){
test(5,"one book","two book");
}
}
也可以使用数组形参来定义方法,效果一样
方法重载:一个类中定义多个同名方法。只要形参列表不同即可。重载有两个要求,两同一不同,即同一个类中方法名相同,参数列表不同,返回值类型,修饰符等与重载没有关系。
方法重载与方法重载的区别:方法重载是在同一类中,方法名相同,参数列表不同(参数顺序,个数,类型),与方法参数名无关。方法重写是在子类继承父类时,子类方法与父类的方法名字相同,而且参数的个数和类型一样,返回值也一样的方法,就称为重写。子类的访问范围需要大于父类的访问范围。
把重写和重载放在一起比较本身没有什么意义,因为重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。两者之间联系很少,除了这都是发生在方法之间,而且要求方法名相同之外,没有太大相似。
成员变量的局部变量
当Java初始化对象时,Java会在堆内存中开辟一块内存,用来存放实例对象。其中num是成员变量。
创建第二个对象p2时,由于在创建第一个对象时,已经对类进行了初始化,所以创建p2时不需要对类进行初始化,对象的创建过程与第一个对象的创建过程没什么区别。
局部变量的初始化和内存中运行机制:
- 局部变量必须经过显示初始化之后才能开始使用,系统不会为局部变量执行初始化。定义了局部变量以后,系统并没有给局部变量进行初始化,直到程序给这个局部变量赋初值的时,系统才会为这个局部变量分配内存空间,并将初始值保存到这块内存中.
- 局部变量不属于任何类或实例,因此它总是保存在方法的栈内存中。如果变量是基本数据类型,该变量直接存储在方法的栈内存中,如果是引用变量则将引用的地址存储在方法的栈内存中。
- 栈内存中变量无需系统垃圾回收,随着方法或代码块的运行结束而结束。局部变量通过只保存具体的值或应用地址,所以地址所占的内存比较小。
隐藏和封装
Java提供了三个访问控制符,private,protected,public来实现对属性或方法的访问控制。
private:当前类访问权限。
default:包访问权限。
protected:子类访问权限。
public:公共访问权限。
深入构造器:构造器完全负责创建Java对象吗?
不是!当调用构造器时,系统会先为这个对象分配内存空间,并为这个对象默认初始化,这个对象已经产生了,这些操作在构造器执行之前就已经完成了。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,这是该对象才可以在外部进行访问。
类的继承
注意:子类只能从父类获得成员变量,方法和内部类(包括内部接口、枚举)不能获得构造和初始化块。(但是子类构造器可以使用super去调用父类构造器)
super关键字用于限定该对象调用从父类继承得到的实例变量和方法。如果子类中定义了和父类同名的实例变量,则会发生子类实例变量隐藏父类实例对象的情形(这里的隐藏不是完全覆盖,系统在创建子类实例对象时,依然会为父类中定义的,被隐藏的变量分配内存空间)。
调用父类构造器:在一个构造器中调用另一个重载的构造器使用this来完成。在子类中调用父类构造器使用super来完成。super调用构造器必须出现在子类子类构造器执行体第一行。子类构造器执行体中既没有super调用也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参构造器。
总之,当调用子类构造器来实例化对象时,父类构造器总会在子类构造器执行前执行。
多态
Java引用变量有两个类型:一个是编译类型,一个是运行类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果两个种类不一致,就会出现多态。因此就会将子类对象赋值给父类引用,成为向上转型,而不用进行类型转换。如:
a的编译类型是Animal,编译时JVM在栈内存中创建的一个引用对象,new Bird()代码运行时JVM在堆内存开辟了一块新的内存,所以a的运行类型时Bird,两者不一致,因此出现了多态。
向上转型:子类是一种特殊的父类,因此Java允许把一个特殊的子类对象直接赋值给一个父类引用,无需任何类型转换,或成为向上转型。
**注意:**引用变量在编译阶段只能调用其编译时所具有的方法,但执行时则执行他运行时类型所具有的方法。因此编写代码只能调用声明该变量时所用类里面包含的方法。
如果需要让变量调用他运行时类型方法,则必须强制转换成运行时类型。强制转换时,需要编译类型为父类类型,而运行时子类类型。在进行强制类型转换转换,先用instanceof判断是否可以转换。使用instanceof时需要注意,instanceof运算符前面操作数的编译时类型要么和后面的类相同,要么与后面的类具有父子继承关系否则会引起编译错误。
类初始化块
Java中三种类初始化块的执行顺序: Java里初始化一个类的对象,通过初始化或构造方法进行数据赋值。与其相关的执行代码由这么几种。
-
静态初始化块
静态初始化块只在类加载时执行一次,同时静态初始化块只能给静态变量赋值,不能初始化普通的成员变量。
-
初始化块
非静态初始化块在每一次初始化实例对象的时候都执行一次,可以给任意变量赋值
-
构造方法
在每次初始化实例对象时调用
执行顺序:
- 在加载类时执行一次静态初始化块(之后不再调用,只执行一次)
- 在每次初始化实例对象时,先执行费非静态初始化块。
- 再执行构造方法
系统在初始化话截断执行类初始化时,不仅会执行本类的初始化块,还会一直上溯到java.lang.Object类,先执行java.lang.Object类的类初始化块,再执行其父类的类初始化块。。。最后才执行该类的类初始化块。
初始化的修饰符只能是static,使用static修饰的初始化时类初始化,没有static修饰的初始化块是实例初始化块。类初始化时类相关的,系统将在系统初始化阶段执行类初始化块(只执行一次),而不是在创建对象时初始化。
public class Test{
public static void main(String []args){
new Leaf();
new Leaf();
}
}
class Root{
static{
System.out.println("Root的类初始化块");
}
{
System.out.println("Root的实例初始化块");
}
public Root(){
System.out.println("Root的无参构造器");
}
}
class Mid extends Root{
static{
System.out.println("Mid的类初始化块");
}
{
System.out.println("Mid的实例初始化块");
}
public Mid(){
System.out.println("Mid无参构造器");
}
public Mid(String s){
this();//调用用一类中重载的构造器
System.out.println("Mid的带参构造器,参数为 "+s);
}
}
class Leaf extends Mid{
static{
System.out.println("Leaf的类初始化块");
}
{
System.out.println("Leaf的实例初始化块");
}
public Root(){
super("Java疯狂讲义");
System.out.println("执行Leaf的构造器");
}
}
root的类初始化块
Mid的类初始化块
Leaf 的类初始化块
root的实例初始化块
root 的无参数构造器
Mid的实例初始化块
Mid 的无参数构造器
Mid的带参数构造器,参数为疯狂Java讲义
Leaf 的实例初始化块
执行Leaf的构造器
root的实例初始化块
root 的无参数构造器
Mid的实例初始化块
Mid 的无参数构造器
Mid的带参数构造器,参数为疯狂Java讲义
Leaf 的实例初始化块
执行Leaf的构造器
执行顺序:
- 先执行父类以及父类的父类的类初始化块
- 再一路执行到自身的类初始化块
- 再执行实例初始化块
- 创建两个实例对象,只要执行一次类初始化块,但实例初始化需要执行两次