二、面向对象基础(中)——封装和继承

1、封装

1.1 封装概述

  • 面向对象编程语言是对客观世界的模拟,客观世界里每一个事物的内部信息都是隐藏在对象内部的,外界无法直接操作和修改,只能通过指定的方式进行访问和修改。封装可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性。随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”,而“高内聚,低耦合”的体现之一:
    • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;
    • 低耦合:仅对外暴露少量的方法用于使用

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

1.2 封装的实现

  • 依赖访问控制修饰符,也称为权限修饰符来控制,把该隐藏的隐藏起来,该暴露的暴露出来。权限修饰符:public,protected,缺省,private
修饰符本类本包其他包子类其他包非子类其他模块
private××××
缺省×××
protected××
public默认不可以,可以建立依赖
  • 访问控制修饰符来控制相应的可见边界,边界有如下:
    • 类,包,子类,模块(Java9之后引入)
  • 各个边界可以使用的权限修饰符
    • 外部类:public和缺省
    • 成员变量:public,protected,缺省,private
    • 成员方法:public,protected,缺省,private
    • 构造器:public,protected,缺省,private

1.3 成员变量/属性私有化问题

  • 成员变量封装的目的
    • 隐藏类的实现细节
    • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
    • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。
  • 成员变量/属性私有化实现步骤
    1. 使用 private 修饰成员变量
    2. 提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下
  • 局部变量与成员变量同名问题
    • 当局部变量与类变量(静态成员变量)同名时,在类变量前面加类名.
    • 当局部变量与实例变量(非静态成员变量)同名时,在实例变量前面加this.

1.4 包(Package)

  • 包的作用
    • 可以避免类重名:有了包之后,类的全名称就变为:包.类名;
    • 分类组织管理众多的类;
    • 可以控制某些类型或成员的可见范围
  • 常见包
    • java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread等,提供常用功能,默认导入
    • java.net----包含执行与网络相关的操作的类和接口
    • java.io ----包含能提供多种输入/输出功能的类
    • java.util----包含一些实用工具类,如集合框架类、日期时间、数组工具类Arrays,文本扫描仪Scanner,随机值产生工具Random
    • java.text----包含了一些java格式化相关的类
    • java.sql和javax.sql----包含了java进行JDBC数据库编程的相关类/接口
    • java.awt和java.swing----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)

1.5 构造器(Constructor)

  • 构造器名必须与它所在的类名必须相同
  • 没有返回值,所以不需要返回值类型,甚至不需要void
  • 如果你不提供构造器,系统会给出无参数构造器,并且该构造器的修饰符默认与类的修饰符相同
  • 如果你提供了构造器,系统将不再提供无参数构造器,除非你自己定义
  • 构造器是可以重载的,既可以定义参数,也可以不定义参数
  • 构造器的修饰符只能是权限修饰符,不能被其他任何修饰
  • 构造器只为实例变量初始化,不为静态类变量初始化

1.6 标准JavaBean

JavaBean是Java语言编写类的一种标准规范。符合JavaBean 的类,要求:

  • 类必须是具体的和公共的
  • 并且具有无参数的构造方法
  • 成员变量私有化,并提供用来操作成员变量的setget 方法

如下:

public class Student{
    private String name;
    private int age;
    
    //默认无参构造方法
    public Student(){    
    }
    
    public Student(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    public void setName(String name){
        this.name = name;
    }
    
    public String getName(){
        return this.name;
    }
    
    public void setAge(int age){
        this.age = age;
    }
    
    public int getAge(){
        return this.age;
    }
    
    //其他成员方法列表
    public String getInfo(){
        return "Student{name=" + this.name + ",age=" + this.age + "}";
    }
}

2、继承

  • 继承的由来**:多个类中存在相同的属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,值需要和抽取出来的类构成某种关系。多个类称为子类,也叫派生类;多个类抽取出来的类称为父类、超类(superclass)或者基类。继承描述的是事物之间的所属关系。父类更通用,子类更具体。通过继承,可以使多种事物之间形成一种关系体系
  • 继承的好处:提高代码的复用性,提高代码的可扩展性,类与类之间产生了关系,是学习多态的前提

2.1 继承的实现

//class A extends B,那么类A继承自类B,B是A的父类,A是B的子类
//这里Pet是Dog的父类,Dog是Pet的子类
//子类可以继承父类所有的属性和方法,构造方法不能继承,private的属性和方法可以继承但不能访问
public class Pet{
    
}

class Dog extends Pet{
    
}

2.2 继承的特点

  • 子类可以继承父类所有的属性和方法,构造方法不能继承,private的属性和方法可以继承但不能访问
  • 当一个类没有显示的extends某一个类的时候,这个类将隐示的extends Object
  • java语言中,继承父类必须是单继承。一个Java类只能有一个父类。
  • Object是所有Java类的总父类。Object的方法可以给每一个Java类继承。
  • 在创建子类对象时,默认先调用父类的无参的构造方法
  • 父类没有无参的构造方法时,子类的构造方法会报错,需要在子类的构造方法中调用super(形参列表)
class Pet{
    public Pet(){
        System.out.println("Constructor in Pet");
    }
}

class Dog extends Pet{
    public Dog(){
        //super();默认有一个隐式的父类的构造方法
        System.out.println("Constructor in Dog");
    }
}

public class TestOne {
    public static void main(String[] args) {
        Dog dog = new Dog();//默认调用父类的无参的构造方法
        /*
        输出
        	Constructor in Pet
			Constructor in Dog
        */
    }
}
2.2.1 成员变量的继承
  • 父类中的成员变量,无论是public还是private,均会被子类继承
  • 子类虽继承了父类的private成员变量,但是不能直接访问,只能通过继承到的父类的get/set方法进行访问
  • 当父子类有重名属性的时候,必须执行就近原则,看调用的方法属于谁,调用的方法必须优先在本类中搜索访问的属性,找到属性就使用本类属性,找不到属性才会去父类中搜索。
  • 当父类的成员变量非私有时,在子类中可以直接访问,所以如果有重名时,就需要加“super."进行区别。super关键字只能在子类中使用。
class Father {
	public int i=1;
	private int j=1;
	public int k=1;
	public int getJ() {
		return j;
	}
	public void setJ(int j) {
		this.j = j;
	}
}

class Son extends Father{
	public int i=2;
	private int j=2;
	public int m=2;
    
    public void test() {
		System.out.println("父类继承的i:" + super.i);
		System.out.println("子类的i:" +i);
		//System.out.println(super.j);//报错,无法直接访问父类的私有属性
		System.out.println("父类继承的j:" +getJ());
		System.out.println("子类的j:" +j);
		System.out.println("父类继承的k:" +k);
		System.out.println("子类的m:" +m);
	}	
}	

public class TestSon{
	public static void main(String[] args){
		Son s = new Son();
		s.test();
	}
}

/*结果输出
父类继承的i:1
子类的i:2
父类继承的j:1
子类的j:2
父类继承的k:1
子类的m:2
*/
2.2.2 成员方法的继承
  • 子类的构造方法必须在自己构造方法的第一行调用父类的一个构造方法。在调用子类构造方法创建子类对象之前,必须调用父类的构造方法创建父类对象。
  • 方法重写:子类编写一个方法签名(方法体以外的其他部分)与父类完全一样的方法,父类的某个方法继承到子类后,方法不适用于子类,可以对方法进行重写,使用@Override。方法一旦重写,再次调用的方法一定是子类的方法
    • 父子类之间方法的名称,参数列表必须相同
    • 子类的返回值类型必须小于等于父类的返回值类型,也就是子类的返回值类型必须是父类返回值类型的子类或子类的子类
    • 子类方法的权限必须大于等于父类方法的权限修饰符:public > protected > 缺省(default) > private
    • static或final方法不能被重写,父类中对子类不可见的方法(private等)不能被重写
  • 方法重载:同一个类中可以有方法的重载,子类可以重载父类的方法,相当于子类中有两个方法,形参列表不同
class Father{
	public void print(int i){
		System.out.println("i = " + i);
	}
}
class Son extends Father{
	public void print(int i,int j){
		System.out.println("i = " + i  ",j = " + j);
	}
}

2.3 无继承状态下加载顺序

包括,普通属性,静态属性,普通方法,静态方法,构造方法,静态代码块,普通代码块的加载顺序

  • 不创建对象
    1. 类被加载进方法区
    2. 多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载,在静态代码块中使用的静态变量必须在使用前声明。静态属性在加载时,会在方法区中分配空间。静态属性和静态代码块只会在类加载的时候调用一次
  • 创建对象
    1. 类被加载进方法区
    2. 多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载,在静态代码块中使用的静态变量必须在使用前声明。静态属性在加载时,会在方法区中分配空间
    3. 创建对象的代码执行
    4. 普通属性加载
    5. 多个普通代码块按顺序执行
    6. 构造方法调用
    7. 方法调用

静态的属性是类被加载时即分配内存空间,而构造方法和普通代码块是创建对象时调用

所以用构造方法和普通代码块给静态成员赋值就来不及了。因此静态成员的赋值应该放在静态代码块中。

2.4 继承状态下加载顺序

包括,普通属性,静态属性,普通方法,静态方法,构造方法,静态代码块,普通代码块的加载顺序

  • 不创建子类 对象
    1. 父类信息加载,子类信息加载
    2. 父类多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载
    3. 子类多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载
  • 创建子类对象
    1. 父类信息先加载,子类信息后加载
    2. 父类多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载
    3. 子类多个静态代码块和多个静态属性按顺序加载或执行,先声明先加载
    4. 先创建父类对象,后创建子类对象
    5. 先加载父类的普通属性,后加载子类的普通属性
    6. 先顺序执行父类的多个代码块,再调用父类的构造方法
    7. 先顺序执行子类的多个代码块,再调用子类的构造方法
    8. 其他方法调用

总体加载原则:
1、创建对象之前:静态属性和静态代码块,先父类后子类
2、创建对象时:创建对象先父类后子类,普通属性加载先父类后子类,普通代码块加载先父类后子类,构造方法调用先父类后子类

public class Test06 {
    public static void main(String[] args) {
        //0
        Sub s = new Sub();
    }
}
class Base{
    Base(){
        //2
        method(100);
    }
    {
        //1
        System.out.println("base");
    }
    public void method(int i){
        //6
        System.out.println("base : " + i);
    }
}
class Sub extends Base{
    Sub(){
        //5
        super.method(70);
    }
    {
        //4
        System.out.println("sub");
    }
    public void method(int j){

        //3
        System.out.println("sub : " + j);
    }
}

3、final、this和super关键字

  • final关键字
    • 修饰类,类不能被继承
    • 修饰方法,方法不能被重写
    • 修饰的变量不能被修改,一般表示常量。final修饰的成员变量没有set方法,并且必须初始化
    • static修饰的变量也没有get和set方法
  • this关键字
    • 不能出现在静态代码块或者静态方法中
    • 在代码块或构造器中,表示正在创建的那个实例对象,在方法中,代表的是调用该方法的对象
    • 使用格式
      • this.成员变量名,在局部变量与当前对象的成员变量重名时,成员变量前加this,重名和没有重名时都可以省略;this.成员变量名先从本类声明的成员变量列表中查找,没有找到,会从父类继承的子类且仍可见的成员变量列表中查找
      • this.成员方法,调用当前对象的成员方法时,都可以加this,也可以都省略,实际开发中都省略;this.成员方法先从本类声明的成员方法列表中查找,没有找到,会从父类继承中且仍可见的成员方法列表中查找
      • this()或this(实参列表),只能调用本类的其他构造器;必须在构造器的首行;若一个类中声明了n个构造器,则最多有n-1个构造器使用this(实参列表),否则会发生递归调用死循环
  • super关键字
    • 含义:当前对象中从父类中引用的
    • 使用前提
      • 通过super引用父类的在子类中仍然可见的成员变量,成员方法或构造方法等
      • 不能在静态代码块或静态方法中使用super
    • 使用格式
      • super.成员变量,在子类中访问父类的成员变量,特别是父类的成员变量与子类成员变量重名时
      • super.成员方法,在子类中调用父类的成员方法,特别是当子类重写了父类的成员方法时
      • super()或super(实参列表),在子类的构造器首行,用于表示调用父类的哪个实例初始化方法

super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。

4、就近原则和追根溯源原则

4.1 找变量

  • 没有super和this
    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量,
    • 如果不是局部变量,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类的(非private,跨包还不能是缺省的)
  • this :代表当前对象
    • 通过this找成员变量时,先从当前执行代码的本类中找,没有的会往上找父类的(非private,跨包还不能是缺省的)。
  • super :代表父类的
    • 通过super找成员变量,直接从当前执行代码所在类的父类找
    • super()或super(实参列表)只能从直接父类找
    • 通过super只能访问父类在子类中可见的(非private,跨包还不能是缺省的)

注意:super和this都不能出现在静态方法和静态代码块中,因为super和this都是存在与对象中的

4.2 找方法

  • 没有super和this
    • 先从当前对象(调用方法的对象)的本类找,如果没有,再从直接父类找,再没有,继续往上追溯
  • this
    • 先从当前对象(调用方法的对象)的本类找,如果没有,再从父类继承的可见的方法列表中查找
  • super
    • 直接从当前对象(调用方法的对象)的父类继承的可见的方法列表中查找

4.3 找构造器

  • this()或this(实参列表):只从本类中,不会再往上追溯
  • super()或super(实参列表):只从直接父类找,不会再往上追溯

5、初始化

(1) 类初始化肯定优先于实例初始化。

(2) 类初始化只做一次。

(3) 实例初始化是每次创建对象都要进行。

5.1 类初始化

1、类初始化的目的:为类中的静态变量进行赋值。

2、实际上,类初始化的过程时在调用一个()方法,而这个方法是编译器自动生成的。编译器会将如下两部分的所有代码,按顺序合并到类初始化()方法体中。

(1)静态类成员变量的显式赋值语句

(2)静态代码块中的语句

3、整个类初始化只会进行一次,如果子类初始化时,发现父类没有初始化,那么会先初始化父类。

每一个类都有一个类初始化方法()方法,然后子类初始化时,如果发现父类加载和没有初始化,会先加载和初始化父类,然后再加载和初始化子类。一个类,只会初始化一次。
在这里插入图片描述

5.2 成员变量初始化

成员变量有默认值

类别具体类型默认值
基本类型整数(byte,short,int,long)0
浮点数(float,double)0.0
字符(char)‘\u0000’
布尔(boolean)false
数据类型默认值
引用类型数组,类,接口null
5.2.1 显式赋值
public class Student{
    public static final String COUNTRY = "中华人民共和国";
	private static String school = "尚硅谷";
	private String name;
	private char gender = '男';
}

显式赋值,一般都是赋常量值

5.2.2 代码块

如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?

  • 静态初始化块:为静态变量初始化
【修饰符】 class 类名{
    static{
		静态初始化
	}
}

  • 实例初始化:为实例变量初始化
【修饰符】 class 类名{
    {
		实例初始化块
	}
}

静态初始化块:在类初始化时由类加载器调用执行,每一个类的静态初始化只会执行一次,早于实例对象的创建。

实例初始化块:每次new实例对象时自动执行,每new一个对象,执行一次。

public class Student{
	private static String school;
	private String name;
	private char gender;
	
	static{
		//获取系统属性,这里只是说明school的初始化过程可能比较复杂
		school = System.getProperty("school");
		if(school==null) {
			school = "尚硅谷";
		}
	}
	{
		String info = System.getProperty("gender");
		if(info==null) {
			gender = '男';
		}else {
			gender = info.charAt(0);
		}
	}
	public static String getSchool() {
		return school;
	}
	public static void setSchool(String school) {
		Student.school = school;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public char getGender() {
		return gender;
	}
	public void setGender(char gender) {
		this.gender = gender;
	}

}
5.2.3 构造器

我们发现,显式赋值和实例初始化块为每一个实例对象的实例变量初始化的都是相同的值,那么我们如果想要不同的实例对象初始化为不同的值,怎么办呢?此时我们可以考虑使用构造器,在new对象时由对象的创建者决定为当前对象的实例变量赋什么值。

注意:构造器只为实例变量初始化,不为静态类变量初始化

为实例变量初始化,再new对象时由对象的创建者决定为当前对象的实例变量赋什么值。

5.2.4 setter方法

一般用于修改成员变量的值。

5.3 实例初始化

1、实例初始化的目的:为类中非静态成员变量赋值

2、实际上我们编写的代码在编译时,会自动处理代码,整理出一个()的类初始化方法,还会整理出一个或多个的(…)实例初始化方法。一个类有几个实例初始化方法,由这个类就有几个构造器决定。

实例初始化方法的方法体,由四部分构成:

(1)super()或super(实参列表) 这里选择哪个,看原来构造器首行是哪句,没写,默认就是super()

(2)非静态实例变量的显示赋值语句

(3)非静态代码块

(4)对应构造器中的代码

特别说明:其中(2)和(3)是按顺序合并的,(1)一定在最前面(4)一定在最后面

3、执行特点:

  • 创建对象时,才会执行,
  • 调用哪个构造器,就是指定它对应的实例初始化方法
  • 创建子类对象时,父类对应的实例初始化会被先执行,执行父类哪个实例初始化方法,看用super()还是super(实参列表)
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

两个猫崽子和你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值