文章目录
0. 前言
- 面向过程:强调的是功能。
- 面向对象:把功能封装进对象,强调具备了功能的对象。
- 面向对象是基于面向过程的,但是他们是侧重点不同的思想。其最大的区别在于:
- 面向过程只在乎哪些功能实现目标
- 而面向对象是对象可以实现目标,再去考虑该对象具备哪些功能去实现目标。
1. 类与对象
1.1 类与对象关系
类与对象关系:类是共性,是一系列个体的抽象;对象是个性,是某个实际的个体。
比如:张三,李四等是一个个具体的对象,而这些个体的抽象叫做人。
1.2 类
1.2.1 类的组成(5大组成部分)
1.2.1.1 成员变量(或成员字段Field)与成员方法 / 普通属性与普通方法
- 成员变量定义:
访问修饰符 + 数据类型 + 变量名
eg:private int age;
注意:可以赋初始值,也可以不赋值,因为成员变量有初始值。但是局部变量必须赋值后才能使用
(除了数组)
,因为其没有默认值。- 成员方法
- 定义:
访问修饰符 + 返回值类型 + 方法名 + (形参列表) + { 方法体 }
- 同一个类中的方法可以直接相互使用,不用创建对象后再使用
- return返回值类型 与 方法定义中的返回值类型 要一致
- 若要返回多个值,则返回值类型可以用数组
【则数组一般定义在被调用的方法中,这样就不用将数组传入,且由于数组被引用会导致数组不会被回收】
1.2.1.2 类变量(或成员字段Field)与类方法 / 静态属性与静态方法
- 类变量
- 定义:【比成员变量多了static】:
访问修饰符 + static + 数据类型 + 变量名
eg:private static int count;
【推荐】- 特点:
- 所有对象会共享该变量。
- 在类加载的时执行。【关于类加载的详细内容】
- 使用:
类名.类变量名
eg:Person.count
【推荐】对象名.类变量名
eg:person1.count
- 若在定义时初始化:
- 直接赋确定值。eg:
public static int count = 100;
- 调用方法。则只能调用静态方法,不能调用普通方法。
- 类方法
- 定义:【比成员方法多了static】
访问修饰符 + static + 返回值类型 + 方法名 + (形参列表) + { 方法体 }
- 特点: 调用时执行。
- 使用:
类名.方法名()
【推荐】对象名.方法名()
- 使用场景:当方法中不涉及任何和对象相关的成员变量或方法,则可以设计为静态方法,提高开发效率。比如:Utils工具类。
- 细节:
- 静态方法中不能含有
this
、super
(因为此时还无对象)- 静态方法中只能访问静态变量和静态方法,不能访问成员变量和成员方法
- 练习
1.2.1.3 代码块
- 代码块:是对构造器的补充,可以做初始化操作。
- 语法:
- 应用场景:多个构造器中都有重复的语句,可以抽取到代码块中,提高代码的重用性。
- 细节:
- 静态代码块是随着类的加载而执行,由于类只加载一次,所以只执行一次;而普通代码块是创建对象时才执行。
- 和静态方法一样。静态代码块只能调用
静态属性
和静态方法
、不能用super
、this
;普通代码块则都可以。
1.2.1.4 构造器 / 构造方法
构造器 / 构造方法:new对象时,系统会自动调用该方法,完成对新对象的初始化。
细节:
- 构造器没有返回值
- 构造器方法名必须与类名一样
- 一旦定义了构造器,默认的无参数构造器方法就会失效。如果需要,则需要重新写上
- 一个类可以定义多个不同的构造器,即构造器重载
1.2.1.5 内部类
1.2.2 类加载
1.3 对象
1.3.1 创建对象
- 直接分配:
Person p = new Person();
- 先声明,再分配。
Person p
:在栈中开辟空间,内容为null,p指向该地址。p = new Person();
:在堆中开辟空间,并将该地址存放到p所指向的空间中,替换掉null。
1.3.2 使用对象
1.3.2.1 对象属性、方法的使用
- 使用对象属性:
对象名.属性名
如:p.name
- 使用对象方法:
对象名.方法名(实参列表)
如:int a = p.getSum(10, 20)
**
1.3.2.2 方法调用的细节
1.3.2.2.1 调用方法时的空间分配
调用方法时,会在jvm的栈中再开辟一块独立的小栈,方法调用完后将结果返回,并释放空间:
- 释放该小栈空间
- 释放其用到且没有被引用的堆空间
- 调用方法的空间分配
- 递归调用方法的空间分配
1.3.2.2.2 传参机制与值返回的本质
传参机制
和 值返回
的本质是赋值,而赋值是复制所在栈地址里的内容。
表现为:实参是基本数据类型时,是值拷贝,不会影响实参;实参是引用数据类型时,是传递地址,会影响实参。
值返回同理。
1.3.3 创建对象的流程分析
1.3.3.1 从类的角度看new对象操作
- 首先是 类加载 。执行
静态代码块
与静态属性
,他们的初始化优先级一样。故看编写的先后顺序。
- 执行对应的构造器,构造器中隐含表示了两个东西:
注意:
super()
内的执行步骤,可以看作new父类。其父类中的静态属性、静态代码块、构造器、普通属性、普通代码块也是一样的执行步骤,有点递归的感觉。普通代码块
与普通属性
的初始化优先级一样。故看编写的先后顺序。- 例子:
当父类也有静态属性、静态方法、普通属性、普通方法时:
记住一点:先把父类的静态执行完,再执行子类静态。后续步骤与上面一致。
例子:
练习:
1.3.3.2 从jvm的角度看new对象操作
- 在jvm栈中开辟独立的小栈main
- Person类加载:完成 加载、连接、初始化 三个操作。类加载详细情况
- 在jvm堆中为实例化对象分配空间,并为属性设置默认值
- 调用父类构造器
- 执行普通属性 和 普通代码块
- 执行子类构造器
- 将实例化对象的地址赋给p
1.3.4 对象的空间分配
数据的存放模式有两点需要注意
- 大端模式、小端模式;
- 边界对齐和边界不对齐(访问某变量:前者访存一次,后者可能一次或多次)。
1.3.5 instanceof关键字
可以用instanceof
,判断某运行对象是否是某类或其子类的对象。
运行对象:指该对象变量名所指向的堆中的对象。
1.3.6 this关键字
- 作用
- 访问当前对象的属性:
this.属性名
- 访问当前对象的方法:
this.方法名(参数列表)
- 访问当前对象构造器:
this(参数列表)
。两个注意点:
- 只能在构造器中访问另外一个构造器,不能在非构造方法中使用。
- 使用this访问构造器,必须在所在构造器的首行
- this关键字在构造函数中不能出现相互调用 的情况,因为是一个死循环。
- 细节
- this 在定义的类的除了静态方法和静态代码块之外的地方使用。
- this 所在单元的内容为该对象的首地址,故this代表当前对象。
1.3.7 final关键字
- final可修饰 类、属性、方法、局部变量。
- 应用场景:
- 当不希望该类被继承时。eg:
public final class Person { }
- 当不希望类的某个属性值被修改时。eg:
private final long TAX_NUM = 100L;
- 当不希望父类的某个方法被子类 覆盖或者重写时.eg:
public final void say() { }
- 当希望某个局部变量的值被修改时。eg:
final int MAX_SIZE = 10;
- 细节:
- final修饰属性或者局部变量,命名规范为:
XX_XX
(大写字母)- final修饰普通属性时的赋值:
- 直接赋值
- 在构造器中赋值
- 在代码块中赋值
- final修饰静态属性时
- 赋值:直接赋值、代码块中赋值。不能在构造器中赋值。
- final和static同时修饰的属性被其他类调用时,不会导致该类的加载。
- final修饰的方法,子类虽然不能覆盖或者重写,但是可以继承
- final不能修饰构造器
- 练习题
2. 访问修饰符号
- 作用:访问修饰符用于修饰类、属性、方法。
对于类只能用public和默认两种
。- 种类
- public:所有包的类公开
- protected:对同一个包中的类,及子类公开
- 默认:对同一个包中的类公开
- private:只对本类公开
3. 理解main方法
- main方法由Java虚拟机调用。
- Java虚拟机与main方法不再同一个包下,所以要用public
- Java虚拟机在执行main方法时不需要创建对象,所以用static
- doc下使用java指令运行程序时,若在后面添加了实际参数,会将这些参数转为字符串数组,即args数组。
4. 作用域
- 全局变量:也就是属性
- 作用域为整个类。
- 全局变量不赋值就可以使用,因为有默认值
- 可以加修饰符,也可以不加
- 局部变量:除了属性之外的其他变量
- 方法中的变量,其作用域在该方法内;控制结构中的变量,其作用域在该代码块中。
- 局部变量必须赋值才可以使用,因为其没有默认值。
- 不能加修饰符
- 细节
1. 属性和局部变量作用域相交时可以重名,访问时遵循就近原则。
2. 局部变量与局部变量作用域相交时不可以重名
5. 包
- 包的作用
- 区分相同名字的类
- 管理类
- 控制访问范围
- 包的本质:创建不同的文件夹来保存类文件
- 包的命名
- 规则
- 字母、数字、_,但是不能以数字开头【比变量命名少个$】
- 不含空格、不能用关键字和保留字
- 规范:
com.公司名.项目名.业务模块名
- 包名一般用小写字母
- 导入包
- 导入com.a包下的一个Person类:
import com.a.Person
- 导入com.a包下的所有类:
import com.a.*
注意:同包下的类,不用导入
- 细节:类在某包下,则第一行必须使用package声明,且该声明必须放在第一行。
如:com.a包下有Person类,则Person类的第一行必须写package com.a
6. 可变参数
- 语法:
数据类型...形参名称
eg:public double getSum(int...nums){ 方法体 }
- 细节点
- 可变参数和数据类型可以一起放在形参列表中,但是可变参数必须放在最后
eg:错误:public double getSum(int...nums, double b){ }
- 形参列表只允许有一个可变参数
eg:错误:public double getSum(int...nums, double...nums2){ }
- 可变参数对应的实参个数可以为0个、1个、多个,但必须是同一类型且与可变参数类型相同
- 可变参数的本质是数组,故当作数组使用
7. overload / 方法重载
overload / 方法重载:同一个类中,方法名相同,但是形参列表不一致。
形参列表是否一致只考虑2个点:【与
形参名称
、返回值类型
无关】
- 参数个数;
- 对应位置的参数类型。与
9. 面向对象的三大特征
9.1 封装
- 封装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。
- 封装的原则:将不需要对外提供的内容都隐藏起来,提供公共方法对其访问。
- 作用:提高安全性、隐藏实现细节
- 实现封装的步骤
- 对于属性,一般都需要隐藏起来,只通过授权的方法访问。实现步骤:
- 用private修饰成员变量
- 设置public的setter和getter方法
- 对于实现具体功能的方法,一般要隐藏起来,实现步骤:
- 用private修饰方法
- 设置public的方法调用这些方法以实现外部者所需的功能
- 细节:将属性封装后,构造器为属性初始化值也应该调用setter来赋值,而不是直接赋值。【可以防止别人用构造器去修改属性值】
9.2 继承
9.2.1 继承的使用
- 继承:子类可以继承父类的所有,并进行扩展。
- 应用场景:当多个类的属性和方法有很多是相同时,且可以抽象出父类,提高代码的复用性。
- 语法:在定义子类时使用
extends
关键字。如:有 父类Person,子类Graduate,则:class Graduate extends Person { 成员变量及方法 }
- 细节
- 所有类都继承了Object类,因为Object是祖先,万物皆对象。
- 子类最多只能继承一个父类。
- B继承A,则B有A的所有属性和方法,同时A可以定义自己特有的属性和方法
- 父类的非私有属性和方法可以在子类中直接被访问,但是私有属性和方法不能在子类中访问,要遵循访问控制。
- 想访问父类的私有属性,可以在父类中定义非私有方法访问私有属性,如getter、setter方法,然后子类调用该方法。
- 想访问父类的私有方法,也类似处理
- 子类的每个构造器都必须
直接
或使用this(参数列表) 间接
调用父类的构造器,完成父类的初始化。
- 默认在子类构造器的第一行调用父类的默认构造器,即无参构造器,等同于
supper();
- 父类没有无参构造器,则必须在子类的构造器中用super指定父类的构造器。语法:
super(参数列表)
- 使用super关键字,必须写在构造器的第一行。
- 由于在构造器中
super
与this(参数列表)
都必须在构造器的首行,故不能共存,只能用一个。
练习:
9.2.2 super关键字
- super代表父类,作用:
- 访问父类的构造器(上面已经提到),只能在子类的构造器中使用。语法:
super(参数列表)
- 访问父类的属性,遵循访问修饰符规则。语法:
super.属性名
- 访问父类的方法,遵循访问修饰符规则。语法:
super.方法名(参数列表)
- 细节
- 当子类和父类的属性或方法重名,用
super.属性名
访问父类属性,用this.属性名
访问子类属性;成员方法同理。
【注意:若不使用super、this关键字加以区分,直接使用,则子类访问某属性或方法时,会先查看子类:若子类有该属性或方法,则访问;否则向上追溯父类】- 子类的每个构造器都必须
直接
或this(参数列表) 间接
调用父类的构造器,完成父类的初始化。
练习:
9.2.3 继承的流程分析
- 将类加载到jvm的方法区。先加载父类,再加载子类
- 在jvm栈中开辟独立的小栈main
- 在jvm堆中为实例化对象分配空间,并为属性设置默认值
- 为实例化对象的属性赋值
- 调用构造器,为实例化对象进行初始化
注意:父类与子类属性重名时,其分别分配了空间,故属性没有重写之说,由于方法在方法区中,故有重写之说。
- 将实例化对象的地址赋给son
- 访问son对象的属性或方法
- 子类和父类的属性或方法重名:首先看子类是否有该属性或方法,有则访问;没有则向上追溯父类有没有这个属性或方法
如:输出son.age
为30,而不是60- 父类的私有属性,子类不能直接访问,但是在推中还是有分配空间。
如:图中的age->60
9.2.4 overwrite / 方法重写
9.2.4.1 方法重写的使用
- 方法重写/覆盖:子类可以对父类的方法进行重写
- 方法重写的判断:
- 方法名、参数列表一致
- 返回值一致,或者是父类的子类型。
如:父类返回Object类型,子类可以返回String类型。- 访问修饰符可以一致或扩大,但不能缩小。
如:public不能改成protected- 细节:默认和private的方法,不能够被重写。(根据访问修饰符范围)
9.2.4.2 重写 vs 重载
9.3 多态
多态:指方法或对象有多种状态,是OOP编程的第三大特征。
9.3.1 方法多态、对象多态、参数多态
- 方法多态:
- 重载体现多态。
- 重写体现多态。
- 对象多态:基于继承实现,指一个父类变量可以接受多个子类对象。表现为对象的编译类型和运行类型不一致。编译类型是确定的,运行类型是可以通过向上转型、向下转型动态改变的。
3个细节:
- 向上转型:可以用父类变量接受子类对象,即向上转型。
如:Animal dog = new Dog();
Animal 是父类,Dog是子类。向上转型注意3点:
- 该父类变量:编译类型是父类类型,但运行类型是子类对象(可以使用dog.getClass()方法验证)。
- 使用该父类变量访问与子类重名的方法时:访问的是子类的方法。原因见下面的动态绑定机制。
使用该父类变量访问与子类重名的属性时:访问的是父类的属性。因为在堆中分别为其分配了空间。
- 由于编译器的限制,导致dog只能调用Animal的属性和方法(遵循访问控制权限,因为其本身是子对象)。尽管Dog专有的属性和方法是存在在于方法区的,但是不能调用。
- 向下转型:由于Dog专有的属性和方法是存在,如果想访问,可以使用向下转型:将父类变量强转为子类变量。
向下转型注意2点:
- 只能强转向上转型后的父类变量,不能强转直接new的父类对象。
- 若父类有多个子类,且子类间无继承关系,则父类变量只能强转为其指向堆中的对象类型。
如:Object是String、Integer的父类,且String、Integer之间无继承关系
- 参数多态:利用对象多态体现参数多态。应用场景:形参为父类类型,实参为子类型,这样有个默认向上转型。表现为父类类型的形参可以接受多种类型的子类对象。
- 对象多态的应用:当某方法重载次数很多时,可使用对象多态简化为一个方法。
9.3.2 动态绑定机制
动态绑定机制与多态是密切相关的,正是由于多态,才引出动态绑定机制。
动态绑定机制:调用向上转型的对象变量a的方法时,该方法、以及该方法所调用的所有方法,都与运行类型绑定。比如:
- 调用对象变量a的方法H,如何找H?
- 调用对象变量a的方法H 且 H方法中再调用方法M时,如何找M???
答:都会优先在a所指向的堆中对象所属类中去找,若没有再到其父类找。
注意:访问对象变量的属性时,没有动态绑定。故:哪个类的方法访问属性,就用哪个类的属性。
9.3.3 多态数组
- 调用数组内对象统一的重名方法
- 调用数组内对象特有的方法:使用
instanceof
和向下转型
10. Object类
10.1 标识对象是否相等
hashCode()和equals()用来标识对象,两个方法协同工作用来判断两个对象是否相等。 对象通过调用 Object.hashCode()生成哈希值,由于不可避免地会存在哈希值冲突的情况。因此hashCode相同时,还需要再调用 equals 进行一次值的比较。但是若hashCode不同,将直接判定两个对象不同,跳过 equals ,这加快了冲突处理效率。
注意:
- 任何时候重写equals,都必须同时重写hashCode。
- 如果 Object.equal () 相等,Object.hashCode () 也必然相等。重写时也建议保证此特性。
10.1.1 hashCode方法
- 哈希码(hashCode):Object的hashCode方法是根据内部地址压缩得到的一个整数。
- 作用:提供该方法是为了支持哈希表,例如HashMap,HashTable等
- 特点:
- 相同对象的哈希值完全一样
- 不同对象的哈希值几乎不一样。
- 继承Object类的类,重写hashCode方法可以指定hashCode值由什么决定。
10.1.2 equals方法
== vs equals方法
==
是比较运算符。可用基本类型和引用类型。基本类型是比较值是否相等;引用类型是比较所指对象(即地址)是否相同,而不是hashCode值。
- 两边都是同类型的变量。
- 一边是变量,另一边是非变量。
equals
是Object类的方法。只用于引用类型。默认比较对象(即地址)是否相同;重写方法后先比较对象(即地址),不相等再比较对象内容(即属性)是否相等。- 重写equal方法三步:
① 比较是否是相同对象
② 比较是否是该对象的实例
③ 将形参强转为该类型,再比较字段是否相同
- 练习
10.3 toString方法
- toString:默认返回类型:
全类名 + @ + 哈希值的十六进制
- 重写toString方法:一般返回类名、属性名及属性值 。【可使用快捷键Alt + insert】
- 细节:直接输出对象名,会自动调用toString方法。
10.4 finalize方法
- finalize方法:当垃圾回收器确定不存在对该对象的任何引用时,由垃圾回收器调用此方法。默认方法是什么也不做。
- 重写finalize方法:加上一些业务逻辑代码,如比如数据库连接释放、关闭文件等等。【在实际开发中一般不重写该方法,了解即可】
- 细节:不是对象一不被任何对象引用就调用finalize方法,是根据jvm的GC算法。
10.5 getClass方法
getClass方法:获取运行对象的类。
通常用对象名.getClass().getName()
获取运行对象的类名
封装和继承目的都是为了代码重用,多态目的是为了接口重用
单例模式
- 单例模式:指在软件运行过程中,某类只有一个实例。
- 实现步骤:
- 设置私有静态属性
- 构造器私有化:防止类外new对象
- 提供一个公共的静态的getInstance()方法:用于获取实例对象
- 实例化对象,有两种情况:
- 饿汉式:在属性处创建对象。类加载时就会创建对象
- 懒汉式:在getInstance方法中创建对象。使用方法时,才创建对象。
- 应用场景:
- 需要生成唯一序列的环境
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 方便资源相互通信的环境
- 饿汉式 vs 懒汉式
- 创建对象的时机不同。
- 饿汉式不存在线程安全问题;懒汉式存在线程安全问题
- 饿汉式存在资源浪费的可能,因为对象不被使用可能会被回收;而懒汉式不存在资源浪费
- 例子:Runtime类就是用的单例模式
冒泡排序:
1. 几趟:length-1趟
2. 每一趟确定一个位置
public static void main(String[] args) {
int[] a = {4, 2, 1, 3, -1, 9, 8};
int temp = 0;
// 1. length-1趟
for (int i = 0; i < a.length-1; i++) {
// 2. 依此确定0,1,2...号位置。使用冒泡依次交换确定
for (int j = a.length-1; j > i; j--) {
if (a[j-1] > a[j]) {
temp = a[j-1];
a[j-1] = a[j];
a[j] = temp;
}
}
}
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + " ");
}
public static void main(String[] args) {
int[] a = {4, 2, 1, 3, -1, 9, 8};
int temp = 0;
// 1. length-1趟
for (int i = 0; i < a.length-1; i++) {
// 2. 依此确定0,1,2...号位置。使用比较非依次交换确定
for (int j = i+1; j < a.length; j++) {
if (a[i] > a[j]) {
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
for (int i = 0; i < a.length; i++)
System.out.print(a[i] + " ");
}