Java基础学习(六):面向对象
- Java基础学习(六):面向对象
- 概念
- 类的结构
- 对象的创建与初始化
- 内存分析
- 封装
- 继承
- 多态
- Instanceof 和 类型转换
- Static 关键字
- 抽象类
- 接口
- 内部类
本文为个人学习记录,内容学习自 狂神说Java
概念
- 面向过程 vs 面向对象
- 面向过程
- 步骤清晰简单,第一步做什么,第二步做什么,...
- 适合处理一些较为简单的问题
- 面向对象
- 分类的思维模式,思考解决该问题需要哪些分类,然后对这些分类进行单独思考
- 分类下的具体细节是面向过程的
- 适合处理复杂的问题
- 面向过程
- 什么是 面向对象
- 面向对象编程(Object-Oriented Programming, OOP)
- 面向对象编程的本质是:以类的形式组织代码,以对象的形式组织(封装)数据
- 核心思想:抽象
- 三大特性:封装、继承、多态
类的结构
类包含了 属性 和 方法 两部分:
public class Student {// 属性String name;int age;// 方法public void study(){// 方法实现}
}
对象的创建与初始化
-
使用 new 关键字创建对象
Student student = new Student();
-
使用 new 关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化,以及对类中 构造器 的调用
-
构造器 也称为构造方法,是在创建对象时必须调用的,有以下特点:
- 必须和类的名字相同
- 必须没有返回类型,也不能写 void
- 构造方法用于初始化,包括 无参构造 和 有参构造
- 如果没有显式定义构造方法,会自动生成一个包含空语句的无参构造
- 如果定义了有参构造但没有定义无参构造,是不会自动生成无参构造的,必须完全没有显式定义才会自动生成
- 快捷键:
Alt + Insert
public class Person {String name;// 无参构造public Person(){this.name = "victoria";}// 有参构造public Person(String name){this.name = name;} }
内存分析
下面通过例子介绍一个基本程序的内存分配过程:
public class Application {public static void main(String[] args) { // main方法作为程序入口Pet dog = new Pet(); // 创建对象dog.name = "a";dog.age = 3;dog.shout();}
}
class Pet {String name;int age;public void shout(){}
}
基本步骤:
- 在方法区中加载 Application 类,存储了 main 方法的具体实现,并将常量添加到常量池中;
- 在栈中创建一个栈帧用于执行 main 方法;
- 由于需要用到 Pet 类,因此在方法区中加载 Pet 类,存储了成员变量的元信息(包括名称、类型等)以及方法的具体实现;
- 执行
Pet dog = new Pet();
时,先在栈中分配引用变量 dog 的空间,用于存储地址; - 之后在堆中分配内存存储该对象的具体信息,name 初始化为 null,age 初始化为0;
- 通过引用变量找到堆中具体地址进行赋值,并根据方法表定位到 shout() 在方法区中的具体实现;
- main 方法执行完毕后栈帧被弹出,回收不再使用的内存。
封装
-
公有(public)和私有(private)
成员变量/方法通过 public 或者 private 修饰,表明该成员变量/方法能否被实例对象访问
public class Application {public static void main(String[] args) {Student s1 = new Student();s1.name = "victoria"; // name是公有的,可访问s1.id = 1; // id是私有的,不可访问,此处会报错} } class Student {public String name;private int id; }
-
get / set
-
为了防止外部随意改动,一般将类的成员变量设置成私有的,而为了能够读写成员变量,一般要提供公有的 get 方法和 set 方法
-
快捷键:
Alt + Insert
public class Application {public static void main(String[] args) {Student s1 = new Student();String name = s1.getName();s1.setName("victoria");} } class Student {private String name; // 成员变量设置成私有的public String getName(){ // get方法,用于获取私有成员变量的值return this.name;}public void setName(String name){ // set方法,用于设置私有成员变量的值this.name = name;} }
-
继承
-
使用关键字 extends 表明继承关系:
public class Student extends Person{}
—— 表明 Student 类继承自 Person 类 -
Student 类继承自 Person 类,则 Student 类称为子类/派生类,Person 类称为父类/基类
-
子类会继承父类的所有 public 和 protected 的属性和方法
-
Java 中所有的类默认继承自 Object 类
-
Java 中类只有单继承,没有多继承(接口可以多继承):一个父类可以有多个子类,但一个子类只能有一个父类
-
访问修饰符:public、protected、private
修饰符 同类 同包 跨包子类 跨包其他类 public √ √ √ √ protected √ √ √ × default(不带修饰符时) √ √ × × private √ × × × -
this 和 super
this 指代当前类,super 指代父类,通过
this.属性/this.方法名()
访问当前类的属性/方法,通过super.属性/super.方法名()
访问父类的属性/方法public class Application {public static void main(String[] args) {Student student = new Student();student.test();} } class Person {protected String name = "a"; // 父类中的成员变量name初始化为"a" } class Student extends Person{private String name = "b"; // 子类中的成员变量name初始化为"b"public void test(){System.out.println(name); // 由于重写的存在,访问的一定是当前类的属性,输出为"b"System.out.println(this.name); // 使用this访问当前类的属性,输出为"b"System.out.println(super.name); // 使用super访问父类的属性,输出为"a"} }
-
构造方法的执行:执行子类构造方法时默认调用了父类的无参构造
public class Application {public static void main(String[] args) {Student student = new Student(); // 创建子类实例} } class Person {public Person(){ // 父类构造方法System.out.println("执行父类构造方法");} } class Student extends Person{public Student(){ // 子类构造方法System.out.println("执行子类构造方法");} } =============================================================================== 输出结果: 执行父类构造方法 执行子类构造方法
本质是在执行子类的构造方法时,第一行默认有一句代码
super();
,如果显式给定该代码也必须放在构造方法的第一行如果需要调用父类的有参构造,需要显式写出:
class Person {public Person(String name){ // 父类为有参构造System.out.println("执行父类构造方法");} } class Student extends Person{public Student(){super("victoria"); // 显式给定父类有参构造的执行System.out.println("执行子类构造方法");} }
-
方法的重写
-
需要有继承关系,子类重写父类的方法
-
方法名、参数列表必须相同
-
修饰符:范围可以扩大但不能缩小,修饰符范围:public > protected > default > private,比如父类是 protected,那么子类可以是 protected 或者 public
-
抛出的异常:范围可以被缩小但不能扩大
-
快捷键:
Alt + Insert
-
方法的重写只和非静态方法有关,也即:子类可以重写父类的非静态方法,无法重写静态方法,静态方法就算有相同的方法名和参数列表也不会被视为重写
-
A 是 B 的子类,A 和 B 有方法名和参数列表相同的静态方法:不会发生重写
public class Application {public static void main(String[] args) {A a = new A();a.test(); // 使用实例对象调用静态方法,不规范但也不会报错,编译时会自动转换成 A.test();B b = new A();b.test(); // 使用实例对象调用静态方法,不规范但也不会报错,编译时会自动转换成 B.test();} } class A extends B{public static void test(){System.out.println("A=>test()");} } class B {public static void test(){System.out.println("B=>test()");} } ================================================================================================== 输出结果: A=>test() B=>test()
-
A 是 B 的子类,A 和 B 有方法名和参数列表相同的非静态方法:会发生重写
public class Application {public static void main(String[] args) {A a = new A();a.test();B b = new A(); // 多态:父类引用可以指向子类对象b.test(); // 由于发生了方法的重写,输出结果为 A=>test()} } class A extends B{public void test(){System.out.println("A=>test()");} } class B {public void test(){System.out.println("B=>test()");} } ================================================================================================== 输出结果: A=>test() A=>test()
-
A 是 B 的子类,A 和 B 有方法名和参数列表相同的方法,但一个是静态一个是非静态:直接报错
-
-
多态
-
一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多
-
父类的引用可以指向子类对象
public class Application {public static void main(String[] args) {Student s1 = new Student();Person s2 = new Student(); // 父类的引用变量指向了子类对象Object s3 = new Student(); // 父类的父类的引用变量指向了子类对象} } class Person {} class Student extends Person {}
需要注意两点区别:
- 等式左边决定:上面的 s2/s3 本质上还是 Person/Object 类的引用变量,它们只能调用父类本身存在的方法
- 等式右边决定:如果 s2/s3 调用的方法经过了 Student 类的重写,则具体执行的是 Student 类中的方法;若没有重写,则执行的还是 Person/Object 类本身的方法
Instanceof 和 类型转换
-
Instanceof :用于判断左侧引用类型变量指向的对象是否为右侧的类或其子类的对象,如果左侧引用类型和右侧类型完全无关则会直接编译报错(编译通不通过取决于引用类型,返回值为true/false取决于具体指向的对象类型)
public class Application {public static void main(String[] args) {// Object > String// Object > Person > Student// Object > Person > TeacherObject object = new Student();System.out.println(object instanceof Student); // trueSystem.out.println(object instanceof Person); // trueSystem.out.println(object instanceof Object); // trueSystem.out.println(object instanceof Teacher); // falseSystem.out.println(object instanceof String); // falsePerson person = new Student();System.out.println(person instanceof Student); // trueSystem.out.println(person instanceof Person); // trueSystem.out.println(person instanceof Object); // trueSystem.out.println(person instanceof Teacher); // falseSystem.out.println(person instanceof String); // 编译报错Student student = new Student();System.out.println(student instanceof Student); // trueSystem.out.println(student instanceof Person); // trueSystem.out.println(student instanceof Object); // trueSystem.out.println(student instanceof Teacher); // 编译报错System.out.println(student instanceof String); // 编译报错} } class Person { } class Student extends Person { } class Teacher extends Person { }
-
对象的类型转换
父类和子类的优先级:父类是高优先级,子类是低优先级,子类可以自动向父类转换,而父类转换成子类需要强制类型转换
Person person = new Student(); // 多态 Student student = (Student)person; // 将Person类的引用变量转换成Student类的变量
Static 关键字
-
静态属性
- 静态变量/类变量:归属于类,在内存中只有一个,被所有实例共享,可以通过
类名.属性名
或者实例名.属性名
调用,推荐前者 - 非静态变量/实例变量:归属于实例,不同实例相互独立,只能通过
实例名.属性名
调用
- 静态变量/类变量:归属于类,在内存中只有一个,被所有实例共享,可以通过
-
静态方法
- 静态方法/类方法:静态方法内只能调用静态方法
- 非静态方法/实例方法:非静态方法内可以调用静态方法和非静态方法
-
静态代码块
静态代码块随着类一同加载,最先执行,只执行一次
public class Block {{System.out.print("匿名代码块 "); // 匿名代码块第二个执行}static {System.out.print("静态代码块 "); // 静态代码块第一个执行}public Block() {System.out.print("构造方法 "); // 构造方法第三个执行}public static void main(String[] args) {Block block1 = new Block();Block block2 = new Block();} } =================================================================================== 输出结果: 静态代码块 匿名代码块 构造方法 匿名代码块 构造方法
-
静态导入包
import
不加static
修饰时只能导入类,而加了static
修饰后能导入类中的具体方法import static java.lang.Math.random; // 导入Math类中的random方法
抽象类
- abstract 修饰符修饰的方法称为抽象方法,修饰的类称为抽象类
- 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类
- 抽象类不能使用
new
关键字来创建对象,它是用来让子类继承的 - 抽象方法只有方法的声明,没有方法的实现,它是用来让子类实现的
- 子类继承抽象类,就必须要实现抽象类没有实现的抽象方法,否则该类也要声明为抽象类
public abstract class Action { // Action为抽象类public abstract void doSomething(); // doSomething为抽象方法
}
接口
-
普通类 vs 抽象类 vs 接口:普通类只有具体实现,抽象类可以有具体实现和规范,接口只有规范
-
声明类的关键字是
class
,声明接口的关键字是interface
-
接口中的所有属性默认都是使用
public static final
修饰的常量,所有方法默认都是使用public abstract
修饰的 -
接口内的成员变量必须初始化,方法只有声明,方法具体实现通过 “实现类” 完成,实现类必须重写接口中的所有方法
public interface UserService { // 接口void run(); // 方法的声明 }
public class UserServiceImpl implements UserService{ // 实现类,使用implements关键字指明对应的接口@Overridepublic void run() { // 重写方法// 具体实现} }
-
接口可以实现多继承
public class UserServiceImpl implements UserService, TimeService{// 具体代码 }
内部类
-
内部类就是在一个类的内部再定义一个类
-
例如,A类中定义了B类,那么B类相对于A类来说就称为内部类,而A类相对于B类就称为外部类
-
内部类的分类:成员内部类、静态内部类、局部内部类、匿名内部类
-
成员内部类
public class Application {public static void main(String[] args) {Outer outer = new Outer();Outer.Inner inner = outer.new Inner(); // 成员内部类的实例化} } class Outer {private int id;public class Inner { // 成员内部类public void getID(){System.out.println(id); // 优势:成员内部类可以获得外部类的私有属性/方法}} }
-
静态内部类
使用
static
对内部类进行修饰,与成员内部类的区别主要在于静态内部类中只能调用静态属性/方法 -
局部内部类
写在方法内的类称为局部内部类
-
匿名内部类
-
匿名对象
public class Test {public static void main(String[] args) {new Apple().eat(); // 没有变量名,无需将实例保存到变量中} } class Apple{public void eat(){} }
-
匿名接口实现类
public class Test {public static void main(String[] args) {new UserService(){ // 同样不给定变量名public void hello(){// 接口中方法的具体实现 }};} } interface UserService {void hello(); }
-
-