一、面向对象概述
1.1 面向过程(POP)与面向对象(OOP)
- 面向过程(Procedure Oriented Programming): 强调功能行为,以函数为最小单位,考虑怎么做
- 面向对象(Object Oriented Programming): 将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做
简单理解:“人把大象装进冰箱”
面向过程:①人打开冰箱门;②人抬起对象,塞进冰箱;③人把冰箱门关闭
面向对象:人{ 打开(冰箱){ 冰箱.开(); } 抬起(大象){ 大象.进入(冰箱) } 关闭(冰箱){ 大象.闭(); } } 冰箱{ 开(){} 闭(){} } 大象{ 进入(冰箱){} }
面向对象本质: 以类的方式组织代码,以对象的形式封装数据【对象是具体的事物,类是对对象的抽象】
面向对象方法分析问题的思路和步骤:
- 根据问题需要,选择问题所针对的现实世界中的实体
- 从实体中寻找解决问题相关的属性和功能,这些属性和功能下形成了概念世界中的类
- 把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义
- 把类实例化成计算机世界中的对象——对象是计算机世界中解决问题的最终工具
二、对象创建
2.1 面向对象的基本元素: 类(Class)和对象(Object)
- 类:对一类事物的描述,是抽象的、概念上的定义
- 对象: 实际存在的该类事物的每个个体,也称为实例(instance)
2.2 类的成员: 属性(Field)和行为(Method)
- 属性:类中的成员变量
- 行为:类中的成员方法
注: 属性(成员变量) 与 局部变量的相同点与不同点
相同点:
- 定义变量格式: 数据类型 变量名 = 变量值
- 先声明,后使用
- 变量都有其对应的作用域
不同点:
- 在类中声明的位置不同:
- 属性:直接定义在类的一对{}内
- 局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
- 权限修饰符的不同:
- 属性:可以在声明属性时,指明其权限,使用权限修饰符-封装性【private、public、缺省、protected】
- 局部变量:不可以使用权限修饰符
- 默认初始化:
- 属性:根据其类型,都有默认初始化值
整型-byte、short、int、long:0
浮点型-float、double:0.0
字符型-char:0或’\u0000’
引用数据类型-类、数组、接口:null- 局部变量:没有默认初始化值,意味着调用局部变量之前一定要显式赋值;特别的,形参调用时赋值即可
- 在内存中加载的位置:
- 属性:堆空间(非static)
- 局部变量:栈空间
2.3 对象创建与调用
- 类的设计
- 对象的创建: 使用 new 关键字
- 对象的调用:调用其属性和方法——“对象.属性” 或 “对象.方法”
注: 如果创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性(非static的)
2.3.1 类中方法的声明与调用
方法:描述类应该具有的功能
方法的声明:
权限修饰符 返回值类型 方法名(形参列表){方法体}
权限修饰符:private、public、缺省、protected
**方法使用时:**可以调用当前类的属性或方法
2.3.2 匿名对象
匿名对象: 创建对象时,没有显示地赋一个变量名
特征: 匿名对象只能调用一次
使用:
class A{
method(B b){}
}
class B{
}
A a = new A();
a.method(new B())
2.4 对象的内存分析
- 堆(Heap):存放对象实例【所有的对象实例及数组都要在堆上分配】
- 栈(Stack):虚拟机栈,存放局部变量【方法执行完毕,自动释放】等;局部变量表存放了编译器可知长度的各种基本数据类型、对象引用(对象在堆内存的首地址)
- 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
2.5 类的成员:构造器 constructor(构造方法)
构造器的作用:
- 创建对象
- 初始化对象的属性
构造器格式:
权限修饰符 类名(形参列表){
}
注:
- 构造器名称和类名相同且没有返回值【没有void】
- 如果没有显式地定义类的构造器,系统则会提供默认提供一个空参的构造器
- 一个类中定义的多个构造器,彼此构成重载
- 一旦显示地定义了类的构造器后,系统将
不再提供默认的空构造器
- 一个类中,至少会有一个构造器
2.6 关键字this
- this修饰属性、方法和构造器:
- this可以理解为
当前对象
或当前正在创建的对象【构造器】
- 可以使用” this.属性 “ 或 “ this.方法 ”的方式,调用当前对象/当前正在创建的对象所属类的属性或方法
- 通常情况下,我们选择省略“this.”,但如果方法/构造器的形参和类的属性重名时,必须显式地使用“this."的方式表明此变量是属性而非形参
- this可以理解为
- this调用构造器
- 在类的构造器中,可以显式地使用
"this(形参列表)"
方式来调用本类中其他的构造器,而不能调用自己 - 规定:
"this(形参列表)"
必须声明在当前构造器首行,意味着只能调用一个其他构造器 - 优点:这样可以避免冗余
- 在类的构造器中,可以显式地使用
注: 有一个注意点
三、面向对象三大特性
面向对象的三大特性:
- 封装(Encapsulation)
- 继承(Inheritance)
- 多态(Polymorphism)
3.1 封装性
程序设计追求“高内聚,低耦合”:
- 高内聚: 类的内部数据操作细节自己完成,不允许外部干涉
- 低耦合: 进队外暴露少量的方法用于使用
封装性: 隐藏对象内部的复杂性、只对外公开简单的接口,便于外界调用,从而提高系统的可扩展性、可维护性【禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问】
封装的体现:
- 将类的属性私有化(priavte)- 避免用户使用“对象.属性”的方式对属性进行赋值,同时提供公共(public)的方法来获取(get)和设置(set)
- (拓展) 不对外暴露的私有的方法
- (拓展)单例模式
权限修饰符:封装性的体现需要权限修饰符来配合
Java规定4种权限(从小到大排列):private、缺省(default)、protected、public
4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类——体现类及类的内部结构在被调用时的可见性的大小
修饰符 | 类内部 | 同一个包 | 不同包的子类 | 同一个工程 |
---|---|---|---|---|
private | Yes | |||
default | Yes | Yes | ||
protected | Yes | Yes | Yes | |
public | Yes | Yes | Yes | Yes |
优点:
- 提高程序的安全性,保护数据
- 隐藏代码的实现细节
- 统一接口【获取和设置属性值使用 get/set方法】
- 系统可维护
3.2 继承性
3.2.1 基本概念
继承性的体现:
class A extends B{}
- A :子类、派生类、subClass
- B:父类、超类、基类、superClass
- 一旦子类A继承父类B以后,子类A就获取了父类B中声明的所有属性和方法
- 当父类中声明为private的属性或方法,子类继承父类之后,仍然认为获取了父类中私有的结构,只是因为封装性的影响,使得子类不能直接调用父类的结构
- 子类继承父类以后,还能声明自己特有的属性或方法-> 实现功能的拓展【子类和父类的关系,不同于子集和集合的关系】
优点:
- 减少代码冗余,提高了代码的复用性
- 便于功能的拓展
- 为之后多态性的使用,提供了前提
注意点:
- Java 只支持单继承或多层继承,不允许多重继承:
- 一个子类只能有一个父类
- 一个父类可以派生出多个子类
- 子类直接继承的父类称为直接父类;间接继承的父类称为间接父类
- 子类继承父类之后,就获取了直接父类以及所有间接父类中 声明的属性和方法
注: IDEA ctrl + H->继承结构
Object类:
- 如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object类
- 所有的Java类(除Java.lang.Object类)都直接或间接的继承于java.lang.Object类,意味着,所有的Java类都具有java.lang.Object类声明的功能
3.2.2 方法重写(overrid/overwrite)
定义: 在子类中可以根据需要对从父类中继承来的方法进行改造即方法的重置、覆盖->在程序执行时,子类对象调用父类中的同名同参数的方法时,实际执行子类重写的代码
要求:
- 子类重写的方法必须和父类被重写的方法具有
相同的方法名称、参数列表
- 子类重写的方法的
返回值类型不能大于
父类被重写的方法的返回值类型
- 重点理解: 父类的返回值类型是A类,则子类重写的方法的返回值类型可以是A类或A类的子类
- 父类void->子类只能是void
- 父类基本类型 -> 子类也必须是基本类型【int 不是 double 的子类,不要和赋值那里的自动提升混淆】- 子类重写的方法
使用的访问权限不能小于
父类被重写的方法的访问权限——特别地,子类不能重写父类中声明为private权限的方法
- 子类
抛出的异常类型不能大于
父类被重写方法的异常类型【相同或是其子类】
注: 子类和父类中的同名同参数的方法要么都声明为非static(这样可以考虑重写),要么都声明为static的(不再是重写)
重载和重写的区别:
- 定义角度:重载是同名不同参,重写是同名同参
- 编译运行角度:
- 重载:这些同名不同参的方法的调用地址在编译器就绑定了【Java的重载可以包括父类和子类,即子类可以重载父类的同名不同参的方法】——“早绑定” 或 “静态绑定”
- 多态:只有等到方法调用的那一刻,解释运行器才会确定所调用的具体方法——“晚绑定” 或 “动态绑定”
3.2.3 super 关键字
简单理解: super == 父类的;可以用来调用父类的属性、方法、构造器
使用:
super调用父类的属性和方法:
- 可以在子类的方法或构造器中,通过使用"super.属性" 或 “super.方法” 的方式,显式 的调用父类声明的属性或方法,但通常情况下习惯省略 “super.”
- 特别地,当子类和父类中定义了同名的属性时,想要在子类中调用父类中声明的属性,必须显式地使用"super.属性"的方式,表明调用的时父类中声明的属性
- 特别地,当子类重写了父类中的方法以后,想要在子类的方法中调用父类被重写的方法时,必须显式地使用"super.方法"的方式,表明调用的时父类中被重写的方法
super调用父类的构造器:
- 可以在子类的构造器中显式的使用"super(形参列表)"的方式【必须在子类构造器的首行】,调用父类中声明的指定的构造器
- 在类的构造器中,针对于"super(形参列表)“和"this(形参列表)” 只能二选一,不能同时出现【都要在首行】
- 在构造器的首行,没有显式的声明"super(形参列表)“和"this(形参列表)”,则默认调用的是父类中空参的构造器即"super()"
- 至少有一个子类的构造器会以 “super” 的方式调用父类的构造器->子类对象实例化过程
3.2.4 子类对象实例化过程
- 从结果上看
子类继承父类以后,就获取了父类中声明的属性或方法,所以创建子类的某个对象后,堆空间中会加载父类中声明的属性
- 从过程上看
当通过子类的构造器创建子类对象时,一定会直接或间接的调用父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类空参的构造器为止——正因为加载过所有的父类的结构,所以才可以看到内存有父类的结构,子类对象才可以考虑调用(这些属性)
注: 虽然创建子类对象时,调用了父类的构造器,但自始自终就创建过一个对象,即为new的对象
3.3 多态性
3.3.1 基本概念
简单理解: 一个事物的多种形态
何为多态: 对象的多态性,即父类的引用指向子类的对象,e.g 动物可以是狗,可以是猫,而狗类和猫类继承了动物这个类
多态的使用: 虚拟方法调用【Virtual Method Invocation】
- 在编译期间(调用期间),只能调用父类中声明的方法,但运行期间,实际执行的是子类重写父类的方法
调用看左边,运行看右边
多态性的使用前提:
① 类的继承关系
② 方法的重写
多态的优点: 函数参数类型设为父类,就可以避免代码繁琐/重复
注:
对象的多态性,只适用于方法,不适用于属性
- “不要犯傻,如果它不是晚绑定,它就不是多态”——Bruce Eckel,即多态是运行时行为
3.3.2 Instanceof 操作符
x instance of A
:检验对象 x 是否为类A的实例,返回值为boolean型——x 所属的类与类A必须是子类的和父类的关系,否则编译错误
使用情景: 为了避免在向下转型时出现 ClassCastException 的异常,可以在向下转型之前,先进行 instanceof的判断:返回true就进行向下转型;返回false,就不进行向下转型
3.3.3 向下转型
问题: 有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法,但由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法,而子类特有的属性和方法不能调用。那么,如何才能调用子类特有的属性和方法?
解决: 向下转型——强制类型转换
注:只有经过向上转型后的对象才能向下转型——转回去
【否则会遇到编译通过而运行不通过或直接编译不通过的问题】
public class interview0 {
public static void main(String[] args) {
Base base = new Sub();
//难点/记忆点,子类是否重写-->形参中的数组与...
base.add(1,2,3);//sub_1
Sub s = (Sub)base;//经过向上转型后,再转回来,就可以使用子类的特有方法了
s.add(1,2,3);//sub_2
}
}
class Base{
public void add(int a,int... arr){
System.out.println("base");
}
}
class Sub extends Base{
//对父类方法的重写
public void add(int a,int[] arr){
System.out.println("sub_1");
}
public void add(int a,int b,int c){
System.out.println("sub_2");
}
四、拓展
4.1 JavaBean
JavaBean是一种用Java语言编写的可重用组件,实际是符合以下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
4.2 UML类图
+表示public类型;-表示private类型;#表示protected类型
属性表示->属性访问类型(+/-) 属性名:属性类型
方法表示->方法访问类型(+/-) 方法名(参数名:参数类型):返回值类型
方法下划线表示为构造器
4.3 MVC设计模式
MVC 是常用的设计模式,将整个程序分为三个层次:视图模型层,控制器层,数据模型层
,使得程序的输入输出、数据处理和数据展示分离开来,这样可以是程序结构变得灵活清晰,吉描述了程序各个对象间的通信方式,降低了程序的耦合性
- 模型层model:主要处理数据
- 控制层controller:处理业务逻辑
- 视图层view:显示数据
4.4 Object类的使用
- Object类是所有Java类的根父类——Object类中的功能(属性和方法)就具有通用性
- 属性:无
- 方法:equals() / toString() / getClass() / hashCode() / clone() / finalize() / wait() / notify() / notifyAll()
- 如果在类的声明中未使用extends关键字指明其父类,则默认父类为 java.lang.Object类
- Object类中只声明了一个空的构造器
4.4.1 equals()
运算符 == 的使用:
- 可以使用在基本数据类型变量和引用数据类型变量中
- 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等(不一定类型要相同)
- 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同即两个引用是否指向同一个对象实体
方法equals()的使用:
- 是一个方法而非运算符
- 只能适用于引用数据类型
- Object 类中equals()的定义:
public boolean equals(Object obj){return (this == obj);}
说明:Object类中定义的equals()同== 的作用是相同的:两个引用是否指向同一个对象- 像String、Date、File、包装类等都重写了Object类中的equals()方法,重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同
- 一般情况下,自定义类需要对Object类的equals()进行重写,想要的不是 == 的比较方式,是对“实体内容”的比较
看一下String类的写法【JDK11】:
public boolean equals(Object anObject) {
if (this == anObject) {//可以参照
return true;
}
if (anObject instanceof String) {//可以参照
String aString = (String)anObject;//可以参照的
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
可以试着按这个套路:
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof B) {
B temp = (B)obj;
return this.att1 == temp.att1&& this.att2.equals(temp.att2)&&... ;
}
return false;
}
自动版:
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;//差不多,加上一个判空
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
面试回答两者区别:
- == 既可以比较基本类型也可以比较引用类型,对于前者就是比较直,后者比较内存地址
- equals属于java.lang.Object类中方法,该方法如果没被重写,效果同==;像String等类中就重写了该方法,比较的是值是否相同
- 实际运用时,要看这个类有没有重写equals方法【通常情况下的重写,就是比较两个对象中对应属性的值是否相同】
4.4.2 toString()
- Object类中的toString()定义:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
说明:该方法的返回值类型是String,返回类名和对象的引用地址
当输出一个对象的引用时,会自动调用toString()- 在进行String与其他数据类型的连接操作时,自动调用toString()方法
- 像String、Date、File、包装类等都重写了Object类中的equals()方法,重写以后,会返回实体内容信息
自定义类也可以重写该方法,从而达到输出属性值的目的
源码体验:
public void println(Object x) {
String s = String.valueOf(x);//注意这里
synchronized (this) {
print(s);
newLine();
}
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();//注意这里
}
4.5 单元测试JUnit【Good!】
具体步骤:
- 创建一个用来单元测试的类:①该类是public的;②该类提供公共的无参的构造器
- 在该类中创建某个单元测试方法:①该方法是public的;②没有返回值;③没有形参
- 在创建的单元测试上声明注解:@Test【此时会报错,因为没有这个包,按下图方式操作】
- 在该方法内写入需要测试的代码
- 点击左侧的绿色箭头即可进行单元测试
4.6 包装类(Wrapper)的使用
Java提供了8种基本数据类型对应的包装类,使得基本数据类型的变量具有类的特征
4.6.1 基本数据类型、包装类、String 三者之间的相互转换
- 基本数据类型与包装类之间的转换
- 基本数据类型–>包装类:调用包装类的构造器——装箱
@Test
public void test1(){
int num1 = 10;
Integer in1 = new Integer(num1);
Integer in2 = Integer.valueOf(num1);//目前的用法
Integer in3 = Integer.valueOf("123");
// Integer in4 = Integer.valueOf("123abc"); //报异常,需要纯粹的数
System.out.println(in1.toString());
System.out.println(in2.toString());
System.out.println(in3.toString());
Float f1 = Float.valueOf(12.35f);
Float f2 = Float.valueOf("12.35f");//也可以不加f
System.out.println(f1.toString());
System.out.println(f2.toString());
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf("true");
Boolean b3 = Boolean.valueOf("true123");//false,不会报异常
//特别地
boolean b4;//false;
Boolean b5;//null,现在是一个类的对象了
}
- 包装类–>基本数据类型:调用包装类的xxxValue——拆箱
@Test
public void test2(){
Integer in1 = Integer.valueOf(10);
int i1 = in1.intValue();
System.out.println(i1+3);
Float F1 = Float.valueOf(2.89f);
float f1 = F1.floatValue();
System.out.println(f1 +6);
}
- 新特性: 自动装箱与自动拆箱
@Test
public void test3(){
int num1 = 22;
method(num1);
//自动装箱
int num2 = 30;
Integer in1 = num2;
boolean b1 = true;
Boolean B1 = b1;
//自动拆箱
int num3 = in1;
}
public void method(Object obj){
System.out.println(obj);//println会用到toString
}
- 基本数据类型、包装类与String之间的转换 【因为基本数据类型与包装类之间可以自动装箱和拆箱】
- 基本数据类型、包装类–>String:调用String重载的ValueOf(Xxx xxx)
@Test
public void Test4(){
int num1 = 56;
//方式1:连接运算
String str1 = num1+"";
//方式2:调用String重载的ValueOf(Xxx xxx)
float f1 = 12.3f;
String str2 = String.valueOf(f1);//"12.3"
Double d1 = Double.valueOf(22.68);
String str3 = String.valueOf(d1);
}
- String–>基本数据类型、包装类:调用包装类的parseXxxx()
@Test
public void Test5(){
String str1 = "123";
// int num1 = (int)str1;//错
// Integer in1 = (Integer)str1;//错,联系向下转型
int num1 = Integer.parseInt(str1);
Integer in1 = Integer.parseInt(str1);
System.out.println(num1);
System.out.println(in1);
}
一些面试题
@Test
public void test1(){
Object o1 = true?Integer.valueOf(1):Double.valueOf(2.5);//三元运算符优先级比甫志高;三元运算符会自动提升类型以保持类型一致
System.out.println(o1);//1.0-->去看println的源码,String.valueOf(o1)->o1.toString(多态)
}
@Test
public void test2(){
Object o2;
if(true)
o2 = Integer.valueOf(1);
else
o2 = Double.valueOf(2.5);
System.out.println(o2);//1
}
@Test
public void Test3(){
//new
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);//false,比较地址
/*Integer有一个Cache数组,存有-128 ~ +127,在这个范围里就直接拿,超出了就得重新new*/
//装箱,在范围里
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(1);
System.out.println(a == b);//true
//自动装箱,在范围里
Integer m = 1;
Integer n = 1;
System.out.println(m == 1);//true
//超出范围得重新new
Integer x = 128;
Integer y = 128;
System.out.println(x == y);//false
}