Part1

一、面向对象(上)

1.对象、引用和指针

  • 对象的创建过程

构造方法是创建对象的重要途径,通过new关键字调用构造器时,返回的也确实是该类的对象,但这个对象并不完全由构造器创建,创建一个对象有以下四步:
(1)分配对象空间,并将对象成员默认初始化为0或false或null,此时对象已经产生,但还不能被外部访问,只能在构造器中通过this引用。
(2)执行属性值的显示初始化。
(3)执行构造方法。
(4)返回对象的地址给引用变量,从而外部程序可以访问该对象。

  1. Person p = new Person();
  2. var p =new Person();//可以使用var来定义局部引用变量

p是引用变量,记录存放在堆内存中实际对象的地址。当一个对象创建后,这个对象就保存在堆内存中,Java不允许直接访问堆内存中的对象,只能通过指向该对象的引用来操作该对象。堆内存里的对象可以有多个引用指向它。
如果堆内存中的对象没有任何变量指向它,那么程序就无法访问该对象,这个对象就变成了垃圾,Java的垃圾回收器将回收该对象,释放该对象所占的内存区。因此,如果希望回收某个对象,只需切断所有指向它的引用,即把这些引用变量赋值为null。
注:数组也是对象,存放在堆内存中,数组名指向这个数组存放的地址,是一个引用变量。
image.png

2.对象的this引用

this关键字最大的作用就是让类中的一个方法,访问该类里的另一个方法或实例变量。this引用总是指向调用该方法的对象,this作为对象的默认引用有两种情形:

  • 构造器中指向该构造器正在初始化的对象。
  • 在方法中指向调用该方法的对象。

如果要在一个方法中调用另一个方法,因为方法只能通过对象来访问,那就必须创建一个对象,这样就会产生两个对象,但这是不必要的,因为当程序调用一个方法时,一定会提供一个对象,可以使用这个已经存在的对象去调用它的其他方法,this就是指向这个已经存在的对象。谁在调用这个方法,this就指向谁。

  1. public class Dog {
  2. public void jump(){
  3. }
  4. public void run(){
  5. var d = new Dog();//新创建一个对象,用它来调用jump()方法
  6. d.jump();
  7. }
  8. public static void main(String[] args){
  9. var dog = new Dog();
  10. dog.run();
  11. }
  12. }
  13. //问题:产生了两个对象,一个Dog对象的run()方法需要依赖另外一个Dog对象的jump()方法,
  14. 不符合逻辑。
  1. //解决:使用this
  2. public class Dog {
  3. public void jump(){
  4. }
  5. public void run(){
  6. this.jump();//通过this指向调用run()方法的对象
  7. }
  8. public static void main(String[] args){
  9. }
  10. }

Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。但调用成员变量、方法时,主调是必不可少的,即使代码中省略了主调,但实际主调仍然存在,一般来说,如果调用static修饰的成员省略了主调,则默认使用该类作为主调;如果调用没有static修饰的成员时省略了主调,则默认使用this作为主调。

  1. ublic class Dog {
  2. public void jump(){
  3. }
  4. public void run(){
  5. jump();//this也可省略,但this依旧存在指向一个对象
  6. }
  7. public static void main(String[] args){
  8. }
  9. }

对于static修饰的方法而言,它属于这个类,并不属于具体的实例,如果在static修饰的方法中使用this,则this无法指向具体的对象。所以,static修饰的方法中不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此Java语法规定:静态成员不能访问非静态成员。
注:Java中有一个让人极易混淆的语法,它允许使用对象来调用static修饰的成员,但实际上是不应该的,static修饰的成员属于类本身,不属于该类的实例,所以就不应该允许使用对象去调用类方法、类变量。在编码时,请不用使用对象去调用类方法、类变量,而应该使用类去调用。

3.深入构造器

  • 使用构造器初始化

构造器最大的作用就是在创建对象时执行初始化。因为构造器主要用来创建该类的对象,通常把构造器设置成public访问权限,允许系统中任何位置的类来创建该类的对象;若需要限制创建该类的对象,可以设置为protected,只能被其子类调用;设置为private,阻止其他类创建该类的实例。

  1. public class Student {
  2. int id;
  3. int age;
  4. String name;
  5. public Student(int id, int age, String name){
  6. this.id = id;
  7. this.age = age;
  8. this.name = name;
  9. System.out.println(id + "," + age + "," + name );
  10. }
  11. public static void main(String[] args){
  12. Student s = new Student(1, 23, "sundegan");
  13. System.out.println(s.id );
  14. System.out.println(s.age );
  15. System.out.println(s.name );
  16. }
  17. }
  • 构造器重载

如果希望有多个初始化过程可供选择,则可以为该类提供多个构造器,就形成了构造器的重载,多个构造器的形参列表不同。

  1. public class Student {
  2. int id;
  3. int age;
  4. String name;
  5. public Student(){ //无参构造器
  6. }
  7. public Student(int id, int age){//两个参数的构造器
  8. this.id = id;
  9. thid.age = age;
  10. }
  11. public Student(int id, int age, String name){//三个参数的构造器
  12. this.id = id;
  13. thid.age = age;
  14. this.name = name;
  15. }
  16. public static void main(String[] args){
  17. Student s1 = new Student();
  18. Student s2 = new Student(1, 23);
  19. Student s3 = new Student(1, 23, "sundegan");
  20. }
  21. }
  • 通过this调用重载的构造器

如果一个构造器B完全包含了构造器A的初始化代码,因为构造器只能通过new关键字来调用,且一旦调用就会生成一个对象,为了能在构造器B中直接调用构造器A中的初始化代码,又不会重新创建一个对象,可以使用this关键字来调用相应的构造器。

  1. public class Student {
  2. int id;
  3. int age;
  4. String name;
  5. public Student(){ //无参构造器
  6. }
  7. public Student(int id, int age){//两个参数的构造器
  8. this.id = id;
  9. thid.age = age;
  10. }
  11. public Student(int id, int age, String name){
  12. this(id, age);//通过this调用上面两个参数的构造器
  13. this.name = name;
  14. }
  15. public static void main(String[] args){
  16. Student s1 = new Student();
  17. Student s2 = new Student(1, 23);
  18. Student s3 = new Student(1, 23, "sundegan");
  19. }
  20. }

注:使用this调用另一个重载的构造器只能在构造器中使用,且必须作为构造器的第一条语句。

4.成员变量和局部变量

成员变量指在类里定义的变量;而局部变量指在方法里定义的变量。
Day3 - 图2

  • 一个类在使用之前要经过类加载、类验证、类准备、类解析、类初始化等几个阶段,其中类变量从类的准备阶段起开始存在,直到系统完全销毁这个类;而实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例。
  • 成员变量无须显示初始化即可使用,系统会在类的准备阶段和创建该类的实例变量时进行默认初始化;但局部变量除形参外,都必须显示初始化后才可访问。
  • 当系统加载类或创建类的实例时,系统会自动未成员变量分配内存空间且进行默认初始化;而局部变量定义后,系统不会为其分配内存空间,直到为这个变量赋初始值后才分配,并将初始值保存到内存中,这就是为什么成员变量可以不显式初始化即可使用,而局部变量一定要进行显式初始化后才能访问。

    1. public class Student{
    2. public int id;//实例变量
    3. public static String name;//类变量
    4. public static void main(String[] args) {
    5. System.out.println("Student的类变量值:" + Student.name);//可直接访问类变量
    6. var s = new Student();
    7. System.out.println("s变量的id为:" + s.id);//可直接访问实例变量
    8. }
    9. }
    10. /*
    11. public class Student{
    12. public static void main(String[] args) {
    13. int a;
    14. System.out.println(a);//报错,a未显示初始化
    15. }
    16. }
    17. */
  • Java允许局部变量和成员变量同名,此时局部变量会覆盖成员变量,如果需要在这个方法里使用被覆盖的成员变量,则可使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。

  • 变量的使用规则:尽可能缩小变量存在的时间和作用域,如果有以下几个情形可考虑使用成员变量。

(1)用于描述某个类或对象的固有信息,如人的年龄、姓名、身高、体重等。
(2)需要以某个变量来保存该类或实例运行时的状态信息。
(3)某个信息需要在该类的多个方法之间进行共享,则这个信息应该使用成员变量保存。

Part2

一、面向对象的三大特征(上)

1.隐藏和封装

1.1 封装的概念(Encapsulation)

封装指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
为了实现良好的封装,需要从以下两个方面考虑:

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
  • 将方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

    1.2 访问控制符的使用

  • 访问控制级别图

image.png

  • private(当前类访问权限):如果类的成员(成员变量、方法和构造器等)使用这个修饰符,则这个成员只能在当前类的内部被访问。适用于修饰成员变量,可以把成员变量隐藏在该类的内部。
  • default(包访问权限):如果类的成员(成员变量、方法和构造器等)或者一个外部类不使用任何访问控制符,就是包访问权限的,包访问权限可以被相同包下的其他类访问。
  • protected(子类访问权限):如果类的成员(成员变量、方法和构造器等)使用这个修饰符,则这个成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。如果使用protected来修饰一个方法,通常是希望其子类重写这个方法。
  • public(公共访问权限):如果类的成员(成员变量、方法和构造器等)或者一个外部类使用这个访问控制符,则这个成员和外部类可以被所有类访问,不管是否处于同一个包中或是否具有父子关系。

image.png

  • 外部类只能使用public和default两种访问控制符,因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此private和protected对外部类没有意义。
  • 如果一个Java源文件里定义的所有类都没有使用public修饰符,则这个源文件的文件名可以是一切合法的文件名;但如果定义了一个public修饰的类,则文件名必须和public修饰的类同名。
  • 访问控制符使用原则:

(1)类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量才考虑用public修饰。除此之外,工具方法也应该使用private修饰,工具方法是用于辅助实现该类的其他方法的方法。
(2)如果某个类主要用作其他类的父类,该类里包含的大部分方法可能仅希望被子类重写,而不想被外界直接调用,应该使用protected。
(3)希望暴露给其他类自由调用的方法应该使用public修饰。因此,构造器通常使用public,从而允许在其他地方创建该类的实例,外部类也通常希望被其他类自由使用,所以大部分外部类也使用public修饰。

1.3 JavaBean规范

如果一个类里的每个实例变量都被private修饰,并未每个变量提供public修饰的setter和getter方法,那么这个类是符合JavaBean规范的类。Java类里实例变量的setter和getter方法具有重要意义,如果一个名为abc的变量,其对应的setter和getter方法应为setAbc()和getAbc(),如果为布尔变量,则getter方法用is开头,如isFlag。

  1. //Student.java
  2. public class Student{
  3. private int age;
  4. public void setAge(int age){
  5. if(age < 0 || age > 120){
  6. System.out.println("设置的年龄不合理!");
  7. }
  8. else{
  9. this.age = age;
  10. }
  11. }
  12. public int getAge(){
  13. return this.age;
  14. }
  15. }
  1. //StudentTest.java
  2. public class StudentTest {
  3. public static void main(String[] args) {
  4. var s = new Student();
  5. //s.age = 23;//报错,age成员变量不可直接访问
  6. s.setAge(23);
  7. System.out.println(s.getAge());
  8. }
  9. }

1.4 包机制(package、import和import static)

包机制是Java中管理类的重要手段,使用包机制,提供了类的多层命名空间,有效解决了类的命名冲、类文件管理等问题。Java允许将一组功能相关的类放在同一个包下,组成逻辑上的类库单元。

  • package

如果希望把一个类放在指定的包结构下,应该在Java源程序的第一个非注释行书写如下格式代码:

  1. package packageName;

使用这个package语句后,源文件里定义的所有类都属于这个包,位于包中的每个类的完整类名都是包名和类名的组合,其他人如果要使用该包下的类,应该使用包名加类名的组合。

  • 包名命名规则:通常域名倒着写,再加上模块名,便于内部管理类。如:cn.sundegan.test、com.oracle.tese等。
  • IDEA里建包:在src目录下单击右键,选择new->package,输入包名即可。如果没有显示指定package语句,则处于默认包下,在实际开发中,不要把类定义在默认包下。
  • JavaJDK中常用包: | java.lang(核心包) | 包含Java语言中的核心类,如String、Math、Integer、Syestem和Thread等 | | —- | —- | | java.net | 包含与网络相关的类 | | java.io | 包含能提供多种输入\输出功能的类 | | java.util | 包含一些实用工具类,如日期(Date)类、日历(Calendar)类、随机数(Random)类,栈(Stack)、向量(Vector)等表示数据结构的类。 | | java.sql | 提供访问数据库的API | | java.text | 包含一些Java格式化相关的类 |

  • import

如果我们需要使用其他包中的类,则可以使用import导入指定包层次下的某个类或全部类,从而可以通过在本类中直接通过类名来调用,否则就需要使用完整的类名。使用import,便于编写代码,提高可读性。一个Java源文件中只能包含一条package语句,但可以包含多条import语句。

  1. import packageName.subpackage.ClassName;//导入某个具体的类
  2. import packageName.subpackage,*;//导入该包下所有的类,会降低编译速度,但不会降低运行速度

Java会默认导入java.lang包下的所有类,因此这些类可直接使用。如果导入两个同名的类,只能使用包名+类名来调用。

  1. import java.sql.Date;
  2. import java.util.*;//该包中也有一个Date类
  3. public class Test{
  4. public static void main(String[] args){
  5. java.util.Date d = new java.util.Date();//使用完整包名区分
  6. }
  7. }
  • import static(静态导入)

用来导入指定类的静态属性和方法,这样就可以直接使用该类下的静态属性和方法。

  1. import static java.lang.Math.*;//导入Math类下的所有静态属性和方法
  2. import static java.lang.Math.PI;//导入Math类下的PI属性
  3. public class Test{
  4. public static void main(String[] args){
  5. System.out.println(PI);
  6. }
  7. }

2.继承

2.1 继承的概念

继承是面向对象的三大特征之一,是实现代码复用的重要手段,同时也便于建模。Java的继承具有单继承的特点(接口可以多继承),每个子类只能有一个直接父类。父类和子类是一种一般和特殊的关系,通过extends关键字来实现继承,是子类对父类的扩展,可以获得父类的全部成员变量、方法和内部类(包括内部接口、枚举),但不一定可以直接访问(如父类的私有属性和方法),Java的子类不能获得父类的构造器和初始化块。

  1. [修饰符] 子类 extends 父类{
  2. /类定义部分
  3. }

如果定义一个类时并未显示指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,Java.lang.Object类是所有类的父类,要么是直接父类,要么是间接父类。因此所有的Java对象都可以调用Object类所定义的实例方法。

2.2 instanof运算符

instanceof是二元运算符,左边是对象,右边是类,当左边的对象是右边类或其子类所创建的对象时,返回true;否则返回false。

2.3 重写父类的方法(Override)

子类通过重写,可以用自身的方法替换父类的方法,方法的重写是实现多态的必要条件。

  • 重写的原则:两同两小一大,两同指方法名、形参列表相同;两小指子类方法返回值类型要比父类方法返回值类型更小或相等,子类方法声明抛出的异常类要比父类方法声明抛出的异常类更小或相等;一大指子类方法的访问权限要比父类方法的访问权限更大或者相等。
  • 覆盖方法和被覆盖方法只能是实例方法,不能是类方法。
  • 重载和重写并无关系,重载主要发生在同一个类的多个同名方法之间;重写发生在父类和子类的同名方法之间。当然,父类和子类间也可以构成重载,因为子类会获得父类的全部方法,如果子类中定义一个与父类方法名相同,但形参列表不同的方法就构成了父类方法和子类方法的重载。
  • 如果父类方法是private访问权限的,则该方法对子类是隐藏的,子类无法访问该方法,也无法重写该方法。如果在子类中定义了一个与父类private方法具有相同方法名、形参列表的、返回值类型的方法,依然不是重写,只是在子类中定义了一个新方法。

    2.4 super限定

  • 当子类覆盖父类的方法后,子类的对象无法访问父类中被覆盖的方法,但可以在子类方法中调用父类被覆盖的方法。如果需要在子类中调用父类中被覆盖的方法,可以通过super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用。

  • 如果在子类中定义了和父类同名的实例变量,则无法在子类中直接访问父类中的实例变量,也可以通过super来访问父类中被覆盖的实例变量。如果被覆盖的是类变量,则可以通过父类名作为调用者来访问被覆盖的类变量。

    1. class BaseClass{
    2. public int a = 1;
    3. public static String s = "abc";
    4. }
    5. public class Subclass extends BaseClass{
    6. public int a = 2;
    7. public static String s = "edf";
    8. public void accessOwner(){
    9. System.out.println(a);//访问自身的实例变量a
    10. System.out.println(s);//访问自身的类变量s
    11. }
    12. public void accessBase(){
    13. System.out.println(super.a);//访问父类的实例变量a
    14. System.out.println(BaseClass.s);//访问父类的类变量s
    15. }
    16. public static void main(String[] args) {
    17. var p = new Subclass();
    18. p.accessOwner();//输出2,edf
    19. p.accessBase();//输出1,abc
    20. }
    21. }
  • 如果在构造器中使用super,则限定该构造器初始化的对象是从父类继承得到的实例变量,而不是该类自己定义的实例变量。

    2.5 调用父类构造器

    子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码。在一个构造器中调用另一个重载的构造器使用this来完成,在子类构造器中调用父类的构造器使用super来完成。

    1. class Person{
    2. public int age;
    3. public Person(int age){
    4. this.age = age;
    5. }
    6. }
    7. public class Student extends Person {
    8. public String name;
    9. public Student(int age, String name){
    10. super(age);//通过super调用父类构造器
    11. this.name = name;
    12. }
    13. public static void main(String[] args) {
    14. var p = new Student(1, "sundegan");
    15. System.out.println(p.age + p.name);
    16. }
    17. }

    使用super调用父类的构造器也必须在第一行,因此super调用和this调用不会同时出现。

    2.6 继承树

    在一个类中,如果构造器的第一行没有显式地使用super来调用父类构造器或者没有使用this调用重载的构造器,Java会自动地调用super(),即调用一个父类的无参构造器。所以,不管是否显式地使用super调用父类构造器,子类构造器总是会调用父类的构造器一次。子类构造器调用父类构造器有以下三种情况:

  • 子类构造器执行体第一行显式使用super显式调用父类构造器。

  • 子类构造器执行体第一行使用this调用另一个重载的构造器,在执行另一个重载的构造器时也会先调用父类的构造器。
  • 子类构造器中即没有super调用,也没有this调用,系统会在子类构造器第一行隐式地调用父类无参构造器super()

不管哪种情况,父类构造器总是在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依次类推,创建任何java对象,最先执行的总是java.lang.Object类的构造器。
Day3 - 图5
从图中可以看出,创建任何对象总是从该类所在继承树的最顶端类的构造器开始执行的,然后依次向下执行,最后才执行本类的构造器。因为所有父类的构造器都被执行过,所有父类都创建了一个对象,有对应的变量和方法,共同存在于整个结构中。当访问其中的一个变量或方法时,先在当前类中查找,如果最底层的子类没有,则依次往上追溯到父类中查找,如果找到则终止;如果都找不到,则报错。
所以,在图中super可以看作是指向父类对象的引用。

  1. class Animal{
  2. private int age;
  3. public Animal(int age){
  4. this.age = age;
  5. }
  6. }
  7. //报错:无法将类 com.sundegan.Animal中的构造器 Animal应用到给定类型
  8. class Dog extends Animal{
  9. }
  10. public class Test {
  11. public static void main(String[] args) {
  12. Dog d = new Dog();
  13. }
  14. }

注:这里会报错:无法将类 com.sundegan.Animal中的构造器 Animal应用到给定类型,为什么?
因为Animal类里显式定义了一个有参构造器,默认的无参构造器不再提供,而在创建Dog对象时,总是会隐式地调用父类的无参构造器,但父类中没有无参构造器,所以报错。以后在显示地定义了有参构造器时,一定要记得再定义一个无参构造器,以便子类隐式调用

2.7 final关键字

final关键字的作用:

  • 修饰变量:被final修饰的变量只能初始化赋值一次,初始化后不可改变。

    1. final double PI = 3.14;
  • 修饰方法:该方法不能被子类重写,但是会被继承,可以在子类中使用,也可以被重载。

    1. final void study();
  • 修饰类:该类不能被继承,如Math类、String类等。