Java语言基础(SE)-第三节 面向对象(一)类定义、对象、内存、构造、包、继承、访问、封装
类的定义、对象的创建
-
类的定义:
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); } }
- 成员变量(Member Variable)也叫做字段(Field)
-
对象的创建
public static void main(String[] args) { Dog dog = new Dog(); dog.age = 20; dog.weight = 5.6; dog.run(); dog.eat("apple"); }
对象的内存
- Java 中所有对象都是 new 出来的,所有对象的内存都是在堆空间,所有保存对象的变量都是引用类型(基本数据类型,放在栈中)
- Java 运行时环境有个垃圾回收器(garbage collector,简称GC),会自动回收不再使用的内存
- 当一个对象没有任何引用指向时,会被GC回收掉内存
对象数组的内存
-
示例代码如下:
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; }
-
内存如下:
- 数组中的元素存储的是每个对象的地址,而不是每个对象的内存
- 思考:方法存储在哪里?
Java程序的内存划分
- Java 虚拟机在执行 Java 程序时会将内存划分为若干个不同的数据区域,主要有
- PC 寄存器(Program Counter Register):存储 Java虚拟机(JVM)正在执行的字节码指令(.class)的地址
- 就相当于X86的IP寄存器,存储CPU将要执行的下一条指令
- Java 虚拟机栈(Java Virtual Machine Stack):存储栈帧
- 调用java语言函数时开辟的栈帧
- 堆(Heap):存储 GC 所管理的各种对象
- 方法区(Method Area):存储每一个类的结构信息(比如字段和方法信息、构造方法和普通方法的字节码(.class)等)
- 类名、成员变量名称、方法名称等
- 本地方法栈(Native Method Stack):用来支持 native 方法的调用(比如用 C 语言编写的方法)
- 调用其他语言函数时开辟的栈帧
- PC 寄存器(Program Counter Register):存储 Java虚拟机(JVM)正在执行的字节码指令(.class)的地址
构造方法(Constructor)
- 构造方法,也叫构造器,能够更方便地创建一个对象
- 方法名必须和类名一样
- 没有返回值类型
- 可以重载
-
建议每个 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
- this 是一个指向当前对象的引用,常见用途是
- 访问当前类中定义的成员变量
- 调用当前类中定义的方法(包括构造方法)
- this 的本质是一个隐藏的、位置最靠前的方法参数
dog.run();
对象调用run方法,但是方法都统一放在方法区- 要想找到某个对象对应的方法,而且在方法中使用该对象的成员
- 那么run方法一定有一个隐藏的参数,这个参数就是引用类型
- 对象调用时将this传递过去,就可以找到调用对象的成员变量
- 因此this是隐藏的、位置最靠前的方法参数
- 只能在构造方法中使用 this 调用其他构造方法;可以在非构造方法中调用其他非构造方法(成员方法)
- 如果在构造方法中调用了其他构造方法
- 构造方法调用语句必须是构造方法中的第一条语句
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)
- 如果一个类没有自定义构造方法,编译器会自动为它提供无参数的默认构造方法
- 一旦自定义了构造方法(无参有参都算),默认构造方法就不再存在
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)
- Java 中的包就是其他编程语言中的命名空间,包的本质是文件夹,常见作用是
- 将不同的类进行组织管理、访问控制(成员访问权限中涉及到了包)
- 解决命名冲突
- 命名建议
- 为保证包名的唯一性,一般包名都是以公司域名的倒写开头,比如 com.baidu.*
- 开发的东西可能给其他公司取用,容易出现相同的包名
- 全小写(以避免与某些类名或者接口名冲突)
- 为保证包名的唯一性,一般包名都是以公司域名的倒写开头,比如 com.baidu.*
- 类的第一句代码必须使用 package 声明自己属于哪个包
- 比如
package com.zh.model;
,这句话表达的就是src目录下的com文件夹/zh文件夹/model文件夹下的类
- 比如
- 如何制定一个包名创建一个类
- 右击src文件夹->new->Class
- Packge输入com.zh:表示创建的类在com文件夹/zh文件夹下
- Name输入类名:Cat
-
Cat中的代码如下:Cat.java文件在src/com/zh文件夹下
//类的第一句代码必须使用 package 声明自己属于哪个包 package com.zh; public class Cat { }
包名的细节
-
如果公司域名有非法字符,建议添加下划线(_)来使包名合法化
域名 软件包名称前缀 //中划线为非法字符 my-name.example.org org.example.my_name //int为关键字 example.int int_.example //不能以数字开头 123name.example.com com.example._123name
导入一个类
- 要想正常使用一个类,必须得知道这个类的具体位置(在哪个包),有 3 种常见方式来使用一个类
-
使用类的全名
com.zh.model.Dog dog = new com.zh.model.Dog();
-
使用 import 导入指定的类名
import com.zh.model.Dog; Dog dog = new Dog();
-
使用 import 导入整个包的所有类
import com.zh.model.*; Dog dog = new Dog();
-
导入的细节
- 为了方便,Java 编译器会为每个源文件自动导入2个包
import java.lang.*;
java.lang
包提供了 Java 开发中最常用的一些类型
-
import 源文件所在包.*;
//默认导入:使用java系统提供的类 package java.lang.*; //默认导入:为了使同一目录下所有的类之间直接相互使用,不需要相互导入 package com.zh.*; package com.zh; public class Cat { }
- 目的是为了在com.zh这个包名下的所有类,都不需要相互导入,直接可以相互使用(注意理解!!)
import aa.bb.*;
- 仅仅是 import 了直接存放在 aa.bb 包中的类型
- 并不包含
import aa.bb.xx.*;
- Eclipse 中导包的快捷键:Ctrl + Shift + O,也可以使用 Ctrl + 1 修复错误来导包
继承(Inheritance)
-
示例代码
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();
-
思考;子类对象的内存中,是否包含父类中定义的 private 成员变量?-依然包含
Object
- 任何类最终都继承自 java.lang.Object,一般称它为基类
同名的成员变量
- 子类可以定义跟父类同名的成员变量(但不推荐这么做)
- 子类对象在内存中会有2个age,并不会覆盖,调用super返回的是父类的age,调用this返回的是当前类的age
方法的重写(Override)
- 重写:子类的实例方法签名与父类一样。也叫做覆盖、覆写
- 格式:在子类方法上面加一行
@Override
即可
- 格式:在子类方法上面加一行
- 重写的注意点
- 子类(重写)override的方法权限必须 ≥ 父类的方法权限
- 就是子类方法的访问权限如果是private,而父类的方法权限是public,这个是不可以的
- 就是成员方法签名用哪个权限关键字修饰
- 假设子类 override 的方法返回值类型是 A,父类的方法返回值类型是 B
- 那么 A == B 或者 A 是 B 的子类型
- 比如父类的方法返回值类型为Object,子类的方法返回值类型为String
- 子类(重写)override的方法权限必须 ≥ 父类的方法权限
super
- 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;
}
}
构造方法的细节
- 子类的构造方法必须先调用父类的构造方法,再执行后面的代码
- 如果子类的构造方法没有显式调用父类的构造方法
- 编译器会自动调用父类无参的构造方法(若此时父类没有无参的构造方法,编译器将报错)
- 举例:
-
默认调用父类无参构造方法
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()
-
父类如果没有无参构造函数,子类会报错
//父类没有无参构造函数 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()"); } }
-
特殊情况
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)
- @Override:告诉编译器这是一个重写后的方法
- 如果不加没有影响,如果加了,当我们重写的方法名、方法签名写错了,编译器会报错
-
@SuppressWarnings(“警告类别”): 让编译器不生成警告信息
//当定义一个变量没有使用时,编译器会自动添加警告,如果不想出现警告 @SuppressWarnings("unused") //如果想取消多种类型的警告,传一个数组 @SuppressWarnings({"rawtypes","unused"})
- @Deprecated: 表示这个内容已经过期,不推荐使用
访问控制(Access Control)
- Java 中有4个级别的访问权限,从高到低如下所示
- public:在任何地方都是可见的
- 修饰成员:main函数的类与Person在不同的包中,然后在main函数中创建一个Person对象,该对象可以访问自己的成员
- protected:仅在自己的包中、自己的子类中可见
- main函数的类与Person必须在相同的包中,然后在main函数中创建一个Person对象,该对象可以访问自己的成员
- 如果Student子类,跟Person不在同一个包中,内部实现中可以访问Person的成员
- 无修饰符(package-private):仅在自己的包中可见
- 同2-1
- 如果Student子类,跟Person必须在同一个包中,内部实现中可以访问Person的成员
- private:仅在自己的类中可见
- 如果Student子类,内部实现中不可以访问Person的成员
- main函数与Person在相同的包中,然后在main函数中创建一个Person对象,该对象不可以访问自己的成员
- public:在任何地方都是可见的
- 使用注意
- 上述4个访问权限都可以修饰类的成员,比如成员变量、方法、嵌套类(Nested Class)等
- 只有 public、无修饰符(package-private)可以修饰顶级类(Top-level Class)
- 一个java文件中一个类中定义嵌套n个类,最外层的,也就是java文件中最外层的那个类就是顶级类
-
举例Person文件
package com.zh; //这里只能写public或者不写,不能写protected、private public class Person { }
-
上述 4 个访问权限不可以修饰局部类(Local Class)、局部变量
public class Person { public void test() { //不能修饰局部变量 private int a = 10; } }
-
一个 Java 源文件中可以定义多个顶级类,但是只能有一个用public修饰,而且这个用public修饰的类名必须跟文件名一致
//Person文件 package com.zh; //只能有一个类公开的public public class Person { } //不能用public修饰 class AAA{} class CCC{}
封装
- 核心:成员变量 private 化,提供 public 的getter、setter
- 编译器快捷,直接输入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方法
- 当打印一个对象时,会自动调用对象的 toString 方法,并将返回的字符串打印出来
- 查看println方法,然后查看valueOf方法,本质调用toString方法
-
toString 方法来源于基类 java.lang.Object,默认实现如下所示
//a string representation of the object. public String toString() { //类名+@+哈希值16进制的形式 return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
- Eclipse 中有一个可以自动生成 getter、setter、constructor、toString 等常用代码的快捷键:
Shift + Alt + S
-
但是直接打印一个对象结果是:
com.zh.Person@7852e922
,因此,我们需要在该对象中重写toStringpublic 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