Java语言基础(SE)-第三节 面向对象(一)类定义、对象、内存、构造、包、继承、访问、封装

类的定义、对象的创建

  1. 类的定义:

     public class Dog {
         //成员变量
         public int age;
         public double weight;
         public void run() {
             System.out.println(age + "_" + weight + "_run");
         }
         public void eat(String food) {
             System.out.println(age + "_" + weight + "_eat_" + food);
         }
     }
    
    1. 成员变量(Member Variable)也叫做字段(Field)
  2. 对象的创建

     public static void main(String[] args) {
         Dog dog = new Dog();
         dog.age = 20;
         dog.weight = 5.6;
         dog.run();
         dog.eat("apple");
     }
    

对象的内存

  1. Java 中所有对象都是 new 出来的,所有对象的内存都是在堆空间,所有保存对象的变量都是引用类型(基本数据类型,放在栈中)
  2. Java 运行时环境有个垃圾回收器(garbage collector,简称GC),会自动回收不再使用的内存
    1. 当一个对象没有任何引用指向时,会被GC回收掉内存

对象数组的内存

  1. 示例代码如下:

     public static void main(String[] args) {
         Dog[] dogs = new Dog[7];
         for(int i = 0; i<dogs.length;i++) {
             dogs[i] = new Dog();
         }
         dogs[6] = null;
     }
    
  2. 内存如下:

    1. 数组中的元素存储的是每个对象的地址,而不是每个对象的内存

图1

  1. 思考:方法存储在哪里?

Java程序的内存划分

  1. Java 虚拟机在执行 Java 程序时会将内存划分为若干个不同的数据区域,主要有
    1. PC 寄存器(Program Counter Register):存储 Java虚拟机(JVM)正在执行的字节码指令(.class)的地址
      1. 就相当于X86的IP寄存器,存储CPU将要执行的下一条指令
    2. Java 虚拟机栈(Java Virtual Machine Stack):存储栈帧
      1. 调用java语言函数时开辟的栈帧
    3. 堆(Heap):存储 GC 所管理的各种对象
    4. 方法区(Method Area):存储每一个类的结构信息(比如字段和方法信息、构造方法和普通方法的字节码(.class)等)
      1. 类名、成员变量名称、方法名称等
    5. 本地方法栈(Native Method Stack):用来支持 native 方法的调用(比如用 C 语言编写的方法)
      1. 调用其他语言函数时开辟的栈帧

构造方法(Constructor)

  1. 构造方法,也叫构造器,能够更方便地创建一个对象
    1. 方法名必须和类名一样
    2. 没有返回值类型
    3. 可以重载
  2. 建议每个 Java 类都提供无参的构造方法

     public class Dog {
         //成员变量
         public int age;
         public double weight;
         public Dog(){}
         public Dog(int age) {
             this.age = age;
         }
         public Dog(int age,double weight) {
             this.age = age;
             this.weight = weight;
         }
     }
        
     Dog dog1 = new Dog();
     Dog dog2 = new Dog(18);
     Dog dog3 = new Dog(20, 6.6);
    

this

  1. this 是一个指向当前对象的引用,常见用途是
    1. 访问当前类中定义的成员变量
    2. 调用当前类中定义的方法(包括构造方法)
  2. this 的本质是一个隐藏的、位置最靠前的方法参数
    1. dog.run();对象调用run方法,但是方法都统一放在方法区
    2. 要想找到某个对象对应的方法,而且在方法中使用该对象的成员
    3. 那么run方法一定有一个隐藏的参数,这个参数就是引用类型
    4. 对象调用时将this传递过去,就可以找到调用对象的成员变量
    5. 因此this是隐藏的、位置最靠前的方法参数
  3. 只能在构造方法中使用 this 调用其他构造方法可以在非构造方法中调用其他非构造方法(成员方法)
  4. 如果在构造方法中调用了其他构造方法
    1. 构造方法调用语句必须是构造方法中的第一条语句
public class Dog {
    //成员变量
    public String name;
    public int age;
    public int price;
    //构造方法
    public Dog(String name,int age, int price) {
        this.name = name;
        this.age = age;
        this.price = price;
    }
    //构造方法中调用已经定义的构造方法
    public Dog(String name) {
        //错误:构造方法调用语句必须是构造方法中的第一条语句
        //this.age = 20;
        //必须放在第一条语句
        this(name,0,0);
        //这么可以
        this.age = 20;
        //错误写法
        //Dog(name,0,0);
    }
    //调用当前类中定义的方法
    public void run() {
        //二者等价
        //eat("");
        this.eat("");//可以在成员方法中调用其他成员方法
        //错误。只能在构造方法中使用 this 调用其他构造方法
        //this(name,0,0);
        System.out.println(age + "_" + weight + "_run");
    }
    public void eat(String food) {}
}

默认构造方法(Default Constructor)

  1. 如果一个类没有自定义构造方法,编译器会自动为它提供无参数的默认构造方法
  2. 一旦自定义了构造方法(无参有参都算),默认构造方法就不再存在
public class Dog {
    //成员变量
    public int age;
    public Dog(int age) {
        this.age = age;
    }
}

Dog dog1 = new Dog(10);//ok
Dog dog2 = new Dog();//error

包(package)

  1. Java 中的包就是其他编程语言中的命名空间,包的本质是文件夹,常见作用是
    1. 将不同的类进行组织管理、访问控制(成员访问权限中涉及到了包)
    2. 解决命名冲突
  2. 命名建议
    1. 为保证包名的唯一性,一般包名都是以公司域名的倒写开头,比如 com.baidu.*
      1. 开发的东西可能给其他公司取用,容易出现相同的包名
    2. 全小写(以避免与某些类名或者接口名冲突)
  3. 类的第一句代码必须使用 package 声明自己属于哪个包
    1. 比如 package com.zh.model;,这句话表达的就是src目录下的com文件夹/zh文件夹/model文件夹下的类
  4. 如何制定一个包名创建一个类
    1. 右击src文件夹->new->Class
    2. Packge输入com.zh:表示创建的类在com文件夹/zh文件夹下
    3. Name输入类名:Cat
    4. Cat中的代码如下:Cat.java文件在src/com/zh文件夹下

       //类的第一句代码必须使用 package 声明自己属于哪个包
       package com.zh;
       public class Cat {
              
       }
      

包名的细节

  1. 如果公司域名有非法字符,建议添加下划线(_)来使包名合法化

     域名                   软件包名称前缀
     //中划线为非法字符
     my-name.example.org   org.example.my_name
     //int为关键字
     example.int           int_.example
     //不能以数字开头
     123name.example.com   com.example._123name
    

导入一个类

  1. 要想正常使用一个类,必须得知道这个类的具体位置(在哪个包),有 3 种常见方式来使用一个类
    1. 使用类的全名

       com.zh.model.Dog dog = new com.zh.model.Dog();
      
    2. 使用 import 导入指定的类名

       import com.zh.model.Dog;
       Dog dog = new Dog();
      
    3. 使用 import 导入整个包的所有类

       import com.zh.model.*;
       Dog dog = new Dog();
      

导入的细节

  1. 为了方便,Java 编译器会为每个源文件自动导入2个包
    1. import java.lang.*;
      1. java.lang 包提供了 Java 开发中最常用的一些类型
    2. import 源文件所在包.*;

       //默认导入:使用java系统提供的类
       package java.lang.*;
       //默认导入:为了使同一目录下所有的类之间直接相互使用,不需要相互导入
       package com.zh.*;
              
       package com.zh;
       public class Cat {
              
       }
      
      1. 目的是为了在com.zh这个包名下的所有类,都不需要相互导入,直接可以相互使用(注意理解!!
  2. import aa.bb.*;
    1. 仅仅是 import 了直接存放在 aa.bb 包中的类型
    2. 并不包含 import aa.bb.xx.*;
  3. Eclipse 中导包的快捷键:Ctrl + Shift + O,也可以使用 Ctrl + 1 修复错误来导包

继承(Inheritance)

  1. 示例代码

     public class Person {
         public int age;
         public void run() {
             System.out.println(age + "_run");
         }
     }
     //extends表示继承
     public class Student extends Person {
         public int no;
         public void study() {
             System.out.println(age + "_" + no + "_study");
         }
     }
        
     Person person = new Person();
     person.age = 15;
     person.run();
     Student student = new Student();
     student.age = 20;
     student.no = 1;
     student.run();
     student.study();
    
  2. 思考;子类对象的内存中,是否包含父类中定义的 private 成员变量?-依然包含

Object

  1. 任何类最终都继承自 java.lang.Object,一般称它为基类

同名的成员变量

  1. 子类可以定义跟父类同名的成员变量(但不推荐这么做)
  2. 子类对象在内存中会有2个age,并不会覆盖,调用super返回的是父类的age,调用this返回的是当前类的age

方法的重写(Override)

  1. 重写:子类的实例方法签名与父类一样。也叫做覆盖、覆写
    1. 格式:在子类方法上面加一行@Override即可
  2. 重写的注意点
    1. 子类(重写)override的方法权限必须 ≥ 父类的方法权限
      1. 就是子类方法的访问权限如果是private,而父类的方法权限是public,这个是不可以的
      2. 就是成员方法签名用哪个权限关键字修饰
    2. 假设子类 override 的方法返回值类型是 A,父类的方法返回值类型是 B
      1. 那么 A == B 或者 A 是 B 的子类型
      2. 比如父类的方法返回值类型为Object,子类的方法返回值类型为String

super

  1. super 的常见用途是:访问父类中定义的成员变量、调用父类中定义的方法(包括构造方法)
public class Person {
    public int age;
    public Person(int age) {
        this.age = age;
    }
}
public class Student extends Person {
    public int no;
    public Student(int no) {
        //调用父类的构造方法
        super(0);
        this.no = no;
    }
}

构造方法的细节

  1. 子类的构造方法必须先调用父类的构造方法,再执行后面的代码
  2. 如果子类的构造方法没有显式调用父类的构造方法
    1. 编译器会自动调用父类无参的构造方法(若此时父类没有无参的构造方法,编译器将报错)
  3. 举例:
    1. 默认调用父类无参构造方法

       public class Person {
           public int age;
           public Person() {
              System.out.println("Person()");
           }
           public Person(int age) {
               System.out.println("Person(age)");
           }
       }
              
       public class Student extends Person {
           public Student() {
               System.out.println("Student()");
           }
       }
              
       Student student = new Student();
       //打印如下,说明编译器自动在Student()方法中第一句添加了super();
       Person()
       Student()
      
    2. 父类如果没有无参构造函数,子类会报错

       //父类没有无参构造函数
       public class Person {
           public int age;
           //手动实现,这编译器不会默认生产无参构造
           public Person(int age) {
               System.out.println("Person(age)");
           }
       }
              
       public class Student extends Person {
           //会报错
           public Student() {
               //必须手动调用父类构造函数,如果父类有n个构造函数,任意调用哪一个都行,但是必须调用,才不会报错
               super(0);
               System.out.println("Student()");
           }
                  
       }
      
    3. 特殊情况

       public class Student extends Person {
           public int no;
           public Student() {
               //这里就不能添加super(0)了,否则报错,因为下面有个this(0)会调用Student(int)构造,函数内部会先调用super(0),因此本质还是先调用了父类的构造
               //super(0);
               this(0)
           }
           public Student(int no) {
               //必须手动调用父类构造函数,才不会报错
               super(0);
               this.no = no;
           }
       }
      

常用注解(Annotation)

  1. @Override:告诉编译器这是一个重写后的方法
    1. 如果不加没有影响,如果加了,当我们重写的方法名、方法签名写错了,编译器会报错
  2. @SuppressWarnings(“警告类别”): 让编译器不生成警告信息

     //当定义一个变量没有使用时,编译器会自动添加警告,如果不想出现警告
     @SuppressWarnings("unused")
     //如果想取消多种类型的警告,传一个数组
     @SuppressWarnings({"rawtypes","unused"})
    
  3. @Deprecated: 表示这个内容已经过期,不推荐使用

访问控制(Access Control)

  1. Java 中有4个级别的访问权限,从高到低如下所示
    1. public:在任何地方都是可见的
      1. 修饰成员:main函数的类与Person在不同的包中,然后在main函数中创建一个Person对象,该对象可以访问自己的成员
    2. protected:仅在自己的包中、自己的子类中可见
      1. main函数的类与Person必须在相同的包中,然后在main函数中创建一个Person对象,该对象可以访问自己的成员
      2. 如果Student子类,跟Person不在同一个包中,内部实现中可以访问Person的成员
    3. 无修饰符(package-private):仅在自己的包中可见
      1. 同2-1
      2. 如果Student子类,跟Person必须在同一个包中,内部实现中可以访问Person的成员
    4. private:仅在自己的类中可见
      1. 如果Student子类,内部实现中不可以访问Person的成员
      2. main函数与Person在相同的包中,然后在main函数中创建一个Person对象,该对象不可以访问自己的成员
  2. 使用注意
    1. 上述4个访问权限都可以修饰类的成员,比如成员变量、方法、嵌套类(Nested Class)等
    2. 只有 public、无修饰符(package-private)可以修饰顶级(Top-level Class)
      1. 一个java文件中一个类中定义嵌套n个类,最外层的,也就是java文件中最外层的那个类就是顶级类
      2. 举例Person文件

         package com.zh;
         //这里只能写public或者不写,不能写protected、private
         public class Person {
         }
        
    3. 上述 4 个访问权限不可以修饰局部类(Local Class)、局部变量

       public class Person {
           public void test() {
               //不能修饰局部变量
               private int a = 10;
           }
       }
      
    4. 一个 Java 源文件中可以定义多个顶级类,但是只能有一个用public修饰,而且这个用public修饰的类名必须跟文件名一致

       //Person文件
       package com.zh;
       //只能有一个类公开的public
       public class Person { }
       //不能用public修饰
       class AAA{}
       class CCC{}
      

封装

  1. 核心:成员变量 private 化,提供 public 的getter、setter
  2. 编译器快捷,直接输入age,就会自动生成age的get方法,直接输入setage就会自动生成age的set方法
public class Person {
    private int age;
    private String name;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    Person person = new Person();
    person.setAge(10);
    person.setName("jack");
    System.out.println(person.getName() + " is age " + person.getAge());
}

toString方法

  1. 当打印一个对象时,会自动调用对象的 toString 方法,并将返回的字符串打印出来
    1. 查看println方法,然后查看valueOf方法,本质调用toString方法
  2. toString 方法来源于基类 java.lang.Object,默认实现如下所示

     //a string representation of the object.
     public String toString() {
         //类名+@+哈希值16进制的形式
         return getClass().getName() + "@" + Integer.toHexString(hashCode());
     }
    
  3. Eclipse 中有一个可以自动生成 getter、setter、constructor、toString 等常用代码的快捷键: Shift + Alt + S
  4. 但是直接打印一个对象结果是:com.zh.Person@7852e922,因此,我们需要在该对象中重写toString

     public class Person {
         private int age;
         private String name;
         ...get、set方法略
         //重写toString
         @Override
         public String toString() {
         	   return "Prson_" + age + "_" + name;
         }
     }
    
     public static void main(String[] args) {
         Person person = new Person();
         person.setAge(10);
         person.setName("jack");
         System.out.println(person);
     }
     //输出结果
     Prson_10_jack
    
Table of Contents