Java 基础-面向对象
前言
学习尚硅谷的《Java零基础全套视频教程(宋红康主讲,java入门自学必备)》的分集73到123。
第2阶段:Java面向对象编程,包括:类及类的内部成员、面向对象的三大特征、其它关键字的使用。
- 第1阶段:Java基本语法
- Java概述、关键字、标识符、变量、运算符、流程控制(条件判断、选择结构、循环结构)、IDEA、数组
- 第2阶段:Java面向对象编程
- 类及类的内部成员
- 面向对象的三大特征
- 其它关键字的使用
- 第3阶段:Java语言的高级应用
- 异常处理、多线程、IO流、集合框架、反射、网络编程、新特性、其它常用的API等
面向对象
学习面向对象内容的三条主线:
- Java类及类的成员:(重点)属性、方法、构造器;(熟悉)代码块、内部类
- 面向对象的特征:封装、继承、多态、(抽象)
- 其他关键字的使用:this、super、package、import、static、final、interface、abstract等
面向对象,是软件开发中的一类编程风格、开发范式。除了面向对象 ,还有面向过程、指令式编程和函数式编程 。在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。
- 面向过程的程序设计思想(Process-Oriented Programming),简称
POP
- 关注的焦点是过程:过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个函数。这样就可以大大简化冗余代码,便于维护。
- 典型的语言:C语言
- 代码结构:以函数为组织单位。
- 是一种“执行者思维”,适合解决简单问题。扩展能力差、后期维护难度较大。
- 面向对象的程序设计思想(Object Oriented Programming),简称
OOP
- 关注的焦点是类:在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。
- 典型的语言:Java、C#、C++、Python、Ruby和PHP等
- 代码结构:以类为组织单位。每种事物都具备自己的属性和行为/功能。
- 是一种“设计者思维”,适合解决复杂问题。代码扩展性强、可维护性高。
类和对象概述
类(Class) 和 对象(Object) 是面向对象的核心概念。
- 类:具有相同特征的事物的抽象描述,是抽象的 、概念上的定义。
- 对象:实际存在的该类事物的每个个体 ,是具体的 ,因而也称为实例(instance)。
类 => 抽象概念的人;对象 => 实实在在的某个人。
Java中用类(class)来描述事物。类,是一组相关属性和行为的集合,这也是类最基本的两个成员。
- 属性:该类事物的状态信息。对应类中的成员变量 。
- 成员变量 <=> 属性 <=> Field
- 行为:该类事物要做什么操作,或者基于事物的状态能做什么。对应类中的成员方法。
- (成员)方法 <=> 函数 <=> Method
面向对象完成功能的三步骤
步骤1:创建类,并设计类的内部成员(属性、方法)
类的定义使用关键字 class。格式如下:
[修饰符] class 类名{
属性声明;
方法声明;
}
// 举例
public class Person{
//声明属性age
int age ;
//声明方法showAge()
public void eat() {
System.out.println("人吃饭");
}
}步骤2:创建类的对象
如何使用 Java 类?创建类的对象,即类的实例化。创建对象的语法如下:
//方式1:给创建的对象命名
//把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了
类名 对象名 = new 类名();
//方式2:
new 类名()//也称为匿名对象[!info] 匿名对象 (anonymous object)
不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。如:new Person().shout();。
使用情况:
- 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象
- 我们经常将匿名对象作为实参传递给一个方法调用。
步骤3:通过对象,调用其内部声明的属性或方法,完成相关的功能
- 对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。
- 使用" 对象名.属性 " 或 " 对象名.方法 "的方式访问对象成员(包括属性和方法)。
类的成员
- 类的内部成员之一:属性、成员变量、field(字段、域)。
- 类的内部成员之二:(成员)方法、函数、method。
- 类的成员之三:构造器(constructor),构造方法
属性
属性 <=> 成员变量 <=>field <=> 字段、域
- 在方法体外,类体内声明的变量称为成员变量。
- 在方法体内部等位置声明的变量称为局部变量。

其中,static将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。接下来先学习实例变量。
| 对比维度 | 成员变量 | 局部变量 |
|---|---|---|
| 声明位置 | 类中,方法(或代码块)外 | 方法体、构造器、代码块内,或方法的形参列表 |
| 存储位置 | 堆内存 (作为对象的一部分) | 栈内存 |
| 生命周期 | 与对象共存亡。随对象创建而创建,随对象被垃圾回收而销毁。 | 与方法/代码块调用共存亡。随方法调用而创建,随方法执行结束而销毁。 |
| 作用域 | 在整个类内部(非静态方法中可直接访问),其他类中通过“对象.变量名”访问。 | 从声明处开始,到所属的代码块({})结束为止。 |
| 修饰符 | public,protected,private,final,volatile,transient等 | 只能使用 final |
| 默认初始化值 | 有。根据数据类型有默认值(如 int 为 0,引用类型为 null)。 | 没有。必须显式初始化(或赋值)后才能使用。形参例外,由调用者传入的实参初始化。 |
| 相同点 | 1. 声明格式相同:数据类型 变量名 [= 初始值]; 2. 必须先声明、初始化,后使用。 3. 都有其作用域,只在作用域内有效。 |
属性的赋值过程
在类的属性中,可以有哪些位置给属性赋值?
- 默认初始化;
- 显式初始化;
- 代码块中初始化
- 构造器中初始化;
- 通过"对象.方法"的方式赋值;
- 通过"对象.属性"的方式赋值;
这些位置执行的先后顺序是怎样?
顺序:1 -> 2/3 -> 4 -> 5/6
由父及子,静态先行。给实例变量赋值的位置很多,开发中如何选?
显式赋值:比较适合于每个对象的属性值相同的场景
构造器中赋值:比较适合于每个对象的属性值不相同的场景
以上操作在对象创建过程中可以执行的次数如何?
- 只能执行一次:1,2,3,4
- 可以多次执行:5,6
方法
方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为 函数 或 过程。
将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码
方法的声明
声明方法的语法格式
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
方法体的功能代码
}一个完整的方法 = 方法头 + 方法体。
- 方法头,就是
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为方法签名 。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。 - 方法体,就是方法被调用后要执行的代码。对于调用者来说,不了解方法体如何实现的,并不影响方法的使用。
方法头可能包含5个部分。
- 修饰符:可选的。方法的修饰符也有很多,例如:public、protected、private、static、abstract、native、final、synchronized等。
- 其中,权限修饰符有public、protected、private。
- 其中,根据是否有static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法。
- 返回值类型: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。
- 方法名:属于标识符,命名时遵循标识符命名规则和规范,“见名知意”。
- 形参列表:表示完成方法体功能时需要外部提供的数据列表。
- throws 异常列表。
Java中的方法不调用,不执行。每调用一次,就执行一次。
方法必须先声明后使用,且方法必须定义在类的内部。
方法内部可以调用类中的方法或属性,不可以在方法内部定义方法。
方法调用内存分析:
- 方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。
- 方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。
- 当方法执行结束后,会释放该内存,称为出栈 。如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。
方法的重载
方法重载(overload):在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。
总结为:“两同一不同”:
- 两同:同一个类、相同的方法名
- 一不同:参数列表不同,包括参数个数不同、参数类型不同
- 注意:方法的重载与形参的名、权限修饰符、返回值类型都没有关系。
重载方法调用:JVM 通过方法的参数列表,调用匹配的方法。先找个数、类型最匹配的,再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错。
可变个数形参的方法
在JDK 5.0 中提供了Varargs(variable number of arguments)机制。即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。
格式:方法名(参数类型 ... 参数名)
//JDK 5.0以前:采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a, String[] books);
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a, String ... books);说明:
- 可变个数形参的方法在调用时,针对可变形参赋的实参的个数可以为:0个、1个或多个
- 可变个数形参的方法与同名的方法之间,可以构成重载
- 特例:可变个数形参的方法与同一个类中方法名相同,且与可变个数形参的类型相同的数组参数不构成重载。
- 可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错。
- 可变个数的形参必须声明在形参列表的最后
- 可变个数的形参最多在一个方法的形参列表中出现一次
构造器
我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢?
可以,Java给我们提供了构造器(Constructor) ,也称为构造方法 。
构造器的作用
- 作用1:搭配new关键字,创建类的对象
- 作用2:在创建对象的同时,可以给对象的相关属性赋值
构造器声明的格式:权限修饰符 类名(形参列表){}
- 构造器名必须与它所在的类名必须相同。
- 它没有返回值,所以不需要返回值类型,也不需要void。
- 构造器的修饰符只能是权限修饰符,不能被其他任何修饰。比如,不能被static、final、synchronized、abstract、native修饰。
[修饰符] class 类名 {
[修饰符] 构造器名() {
// 实例初始化代码
}
[修饰符] 构造器名(参数列表) {
// 实例初始化代码
}
}- 创建类以后,在没有显示提供任何构造器的情况下,系统会默认提供一个空参的构造器,且构造器的权限与类声明的权限相同。
- 一旦类中显示声明了构造器,则系统不再提供默认的空参的构造器。
- 一个类中可以声明多个构造器,彼此之间构成重载。
// 重载
public Person(){
System.out.println("Person()....");
}
public Person(int age) {
this.age = age;
}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}代码块
如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑代码块(或初始化块)。
代码块的作用:用来初始化类或对象的信息(即初始化类或对象的成员变量)。
代码块的分类:
- 一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block)
- 没有使用static修饰的,为非静态代码块。
静态代码块
在代码块的前面加static,就是静态代码块。
【修饰符】 class 类 {
static {
静态代码块
}
}- 可以有输出语句。
- 可以对类的属性、类的声明进行初始化操作。
- 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
- 若有多个静态代码块或静态成员变量,那么按照从上到下的顺序依次执行。
- 静态代码块的执行要先于非静态代码块。
- 静态代码块随着类的加载而加载,且只执行一次。
非静态代码块
如果多个重载的构造器有公共代码,并且这些代码都是先于构造器执行的,那么可以将这部分代码抽取到非静态代码块中,减少冗余代码。
- 可以有输出语句。
- 可以对类的属性、类的声明进行初始化操作。
- 除了调用非静态的结构外,还可以调用静态的变量或方法。
- 若有多个非静态代码块或非静态成员变量,那么按照从上到下的顺序依次执行。
- 每次创建对象的时候,都会执行一次。
顺序:
(1)加载类:静态变量和静态代码块
(2)创建对象时:非静态变量和代码块,然后是构造函数内部类
当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。
将一个类A定义在另一个类B里面,里面的那个类A就称为内部类(InnerClass),类B则称为外部类(OuterClass)。
总的来说,内部类遵循高内聚、低耦合的面向对象开发原则。
内部类使用举例:
- Thread类内部声明了State类,表示线程的生命周期
- HashMap类中声明了Node类,表示封装的key和value
内部类的分类:(参考变量的分类)
- 成员内部类:直接声明在外部类的里面。
- 静态成员内部类
- 非静态成员内部类
- 局部内部类:声明在方法内、构造器内、代码块内的内部类。
- 静态局部内部类
- 非静态局部内部类
实例化静态内部类
外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();实例化非静态内部类
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();类的特征
封装
所谓封装(encapsulation),就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息。
通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
Java实现数据封装的方式是,控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。
Java 规定了四种权限修饰符,分别是 public 、 protected 、 缺省 、 private 。具体访问范围如下:
| 修饰符 | 本类内部 | 本包内 | 同模块其他包的子类 | 同模块其他包的非子类 |
|---|---|---|---|---|
| private | √ | × | × | × |
| 缺省 | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
本包内:package 语句相同的类,在同一个包下。
具体修饰的结构:
- 外部类:public、缺省。
- 成员变量、成员方法、构造器、成员内部类:可以使用4种权限修饰进行修饰。
外部类要跨包使用,必须是 public,否则仅限于本包使用。
成员要在本包下使用,其权限修饰符可以是 public、protected、缺省。
成员要跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义。
开发中4种权限使用频率的情况:
- 比较高:public、private
- 比较低:缺省、protected
Java 中封装性的体现:
场景1:私有化(private)类的属性,提供公共(public)的get和set方法,对此属性进行获取或修改
场景2:将类中不需要对外暴露的方法,设置为private.
场景3:单例模式中构造器private,避免在类的外部创建实例。
继承
- 自上而下:定义了一个类A,在定义另一个类B时,发现类B的功能与类A相似,考虑类B继承于类A。
- 自下而上:定义了类B,C,D等,发现B、C、D有类似的属性和方法,则可以考虑将相同的属性和方法进行抽取,封装到类A中,让类B、C、D继承于类A,同时,B、C、D中的相似的功能就可以删除了。
继承的特点:
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了
is-a的关系,为多态的使用提供了前提。- 继承描述事物之间的所属关系,这种关系是:
is-a的关系。可见,父类更通用、更一般,子类更具体。
- 继承描述事物之间的所属关系,这种关系是:
- 局限性:类的单继承性。后续我们通过类实现接口的方式,解决单继承的局限性。
通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
其中,
- 类B,称为子类、派生类(derived class)、SubClass
- 类A,称为父类、超类、基类(base class)、SuperClass
- 顶层父类是Object类。所有的类默认继承 Object,作为父类。
- 一个类只能有一个父类,不可以有多个直接父类。
有了继承性以后:
- 子类就获取到了父类中声明的所有的属性和方法。
- 但是由于封装性的影响,子类可能不能直接调用父类中的属性或方法。
- 子类在继承父类以后,还可以扩展自己特有的功能(体现:增加特有的属性、方法)
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。
而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循
从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
所以继承意味着子类的对象除了看子类的类模板,还要看父类的类模板。
方法的重写
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?
子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。
方法重写的要求
- 子类重写的方法
必须和父类被重写的方法具有相同的方法名称、参数列表。 - 子类重写的方法的返回值类型
<=父类被重写的方法的返回值类型。(例如:Student < Person)。- 如果返回值类型是基本数据类型和 void,那么必须是相同的
- 子类重写的方法使用的访问权限
>=父类被重写的方法的访问权限。(public > protected > 缺省 > private)- 注意:父类声明为 private 的方法不能重写;跨包的父类缺省的方法也不能重写
- 子类方法抛出的异常
<=父类被重写方法的异常
此外,子类与父类中同名同参数的方法,必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的。
多态
多态的使用前提:1. 类的继承关系;2. 方法的重写。
多态广义上的理解:子类对象的多态性、方法的重写;方法的重载。
多态狭义上的理解:子类对象的多态性。
多态的格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;我对多态的理解:左边的父类类似数学中的未知量,右边的子类就是具体的数值。我们不可能列举出所有的数值,于是用未知数来表示。
这样,我们不必写出每一个具体的式子,我们可以将多个具体的值代入一个式子里求解。
Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译看左边;运行看右边。
若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)。
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
- 编译时,看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
- “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)。
- 运行时,看子类,如果子类重写了方法,一定是执行子类重写的方法体;
- 对于实例变量则不存在多态。即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量,也就是说,子类保留自己的属性外,还继承了所有来自父类的属性。
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法。
- 父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
- 屏蔽了子类特有的属性和方法。
多态的好处:减少了大量的重载的方法的定义;开闭原则。
[!info] 开闭原则OCP
对扩展开放,对修改关闭。
通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能。
向上转型与向下转型
一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。
但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
- 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
- 向下转型:当左边的变量的类型(子类)< 右边对象/变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过
isInstanceof关键字进行判断

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A说明:
- 只要用 instanceof 判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException 异常。
- 如果对象a属于类A的子类B,a instanceof A值也为true。
接口
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。继承是一个"是不是"的is-a关系,而接口实现则是 "能不能"的 has-a 关系。
接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,枚举,接口,注解。
接口的声明格式
[修饰符] interface 接口名 {
//接口的成员列表:
// 公共的静态常量,使用public static final修饰
// 公共的抽象方法
// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}
public interface USB3 {
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s
//抽象方法
void in();
void out();
//默认方法
default void start() {
System.out.println("开始");
}
default void stop() {
System.out.println("结束");
}
//静态方法
static void show() {
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}在JDK8.0 之前,接口中只允许出现:
- 公共的静态的常量:其中
public static final可以省略 - 公共的抽象的方法:其中
public abstract可以省略
理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现
在JDK8.0 时,接口中允许声明默认方法和静态方法:
- 公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略
- 公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略
在JDK9.0 时,接口又增加了私有方法。
除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。
接口的使用
接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同。
// 【修饰符】 class 实现类 implements 接口 {
【修饰符】 class 实现类 extends 父类 implements 接口 {
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
// 接口中的静态方法不能被继承也不能被重写
}接口的多实现(implements):类实现接口
之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
接口的多继承(extends):接口继承接口
一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。
使用接口的方法
- 接口的静态方法,不能被继承,也不能被重写。只能通过“接口名.静态方法名”进行调用。
- 接口的抽象方法、默认方法,只能通过实现类对象才可以调用
- 接口不能直接创建对象,只能创建实现类的对象
public class TestMobileHDD {
public static void main(String[] args) {
//创建实现类对象
MobileHDD b = new MobileHDD();
//通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
b.start();
b.in();
b.stop();
//通过“接口名.”调用接口的静态方法 (JDK8.0才能开始使用)
USB3.show();
//通过“接口名.”直接使用接口的静态常量
System.out.println(USB3.MAX_SPEED);
}
}接口的多态性
接口名 变量名 = new 实现类对象;
// Bullet类继承了Flyable接口
Flyable f1 = new Bullet();[!info] 类优先原则
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。
public class Son extends Father implements Friend {
@Override
public void date() {
//(1)不重写默认保留父类的
//(2)调用父类被重写的
// super.date();
//(3)保留父接口的
// Friend.super.date();
//(4)完全重写
System.out.println("跟康师傅学Java");
}
}[!info] 接口冲突
当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?选择保留其中一个,通过“接口名.super.方法名"的方法选择保留哪个接口的默认方法。
当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?重写默认方法。
public class Girl implements Friend,BoyFriend{
@Override
public void date() {
//(1)保留其中一个父接口的
// Friend.super.date();
// BoyFriend.super.date();
//(2)完全重写
System.out.println("跟康师傅学Java");
}
}小贴士:
接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。
子类重写父接口的默认方法,要去掉default,
子接口重写父接口的默认方法,不要去掉default。
枚举
枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。
枚举类的实现:
- 在JDK5.0 之前,需要程序员自定义枚举类型。
- 对象如果有
实例变量,应该声明为private final(建议,非必须),并在构造器中初始化 私有化类的构造器,保证不能在类的外部创建其对象- 在类的内部创建枚举类的实例。声明为:
public static final,对外暴露这些常量对象
- 对象如果有
- 在JDK5.0 之后,Java支持
enum关键字来快速定义枚举类型。
//jdk5.0之前定义枚举类的方式
class Season {
//1. 声明当前类的对象的实例变量,使用private final修饰
private final String SEASONNAME;//季节的名称
private final String SEASONDESC;//季节的描述
//2. 私有化类的构造器
private Season(String seasonName, String seasonDesc) {
this.SEASONNAME = seasonName;
this.SEASONDESC = seasonDesc;
}
//3. 提供实例变量的get方法
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
//4. 创建当前类的实例,需要使用public static final修饰
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "白雪皑皑");
}enum方式定义
- 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
- 列出的实例系统会自动添加
public static final修饰。 - 编译器给枚举类默认提供的是 private 的无参构造。写常量对象列表时也不用加参数。
- 如果枚举类需要的是有参构造,需要手动定义,有参构造的 private 可以省略。调用有参构造的方法就是,在
常量对象名后面加(实参列表)。 - 枚举类默认继承的是
java.lang.Enum类,因此不能再继承其他的类型。 - JDK5.0 之后switch,提供支持枚举类型,case后面可以写枚举常量名,无需添加枚举类作为限定。
【修饰符】 enum 枚举类名{
常量对象列表;
对象的实例变量列表;
}
public enum Week {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
public enum SeasonEnum {
// public static final Season SPRING = new Season("春天", "春风又绿江南岸");
// 缩写为下面这种形式
//1. 必须在枚举类的开头声明多个对象。对象之间使用,隔开
SPRING("春天", "春风又绿江南岸"),
SUMMER("夏天", "映日荷花别样红"),
AUTUMN("秋天", "秋水共长天一色"),
WINTER("冬天", "窗含西岭千秋雪");
private final String seasonName;
private final String seasonDesc;
private SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
}Enum类中常用的方法
String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法- (关注)
static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值 - (关注)
static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。 String name():得到当前枚举常量的名称。建议优先使用toString()。int ordinal():返回当前枚举常量的次序号,默认从0开始
实现接口的枚举类:和普通 Java 类一样,枚举类可以实现一个或多个接口。
- 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
- 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2 {
//抽象方法的实现
}
//2、如果枚举类的常量可以继续重写抽象方法!
enum A implements 接口1,接口2 {
常量名1(参数) {
//抽象方法的实现或重写
},
常量名2(参数) {
//抽象方法的实现或重写
},
//...
}注解
注解(Annotation)是从 JDK5.0 开始引入,以 @注解名 在代码中存在。
注解可以像修饰符一样被使用,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。还可以添加一些参数值,这些信息被保存在 Annotation 的 “name=value” 对中。
注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。
- 对于单行注释和多行注释是给程序员看的。
- 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。注解可以在类编译、运行时进行加载,体现不同的功能。
注解的重要性
在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE/Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。
未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,Struts2有一部分也是基于注解的了。注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式。
生成文档相关的注解
@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写在编译时进行格式检查(JDK内置的三个基本注解)
@Override 限定重写父类方法,该注解只能用于方法
@Deprecated 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择
@SuppressWarnings 抑制编译器警告。当我们不希望看到警告信息的时候,可以使用SuppressWarnings来抑制警告信息跟踪代码依赖性,实现替代配置文件功能
- Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
- Spring框架中关于“事务”的管理
元注解
元注解:对现有的注解进行解释说明的注解。
JDK1.5 在 java.lang.annotation包定义了4个标准的 meta-annotation 类型,它们被用来提供对其它 annotation 类型作说明。
@Target:用于描述注解的使用范围- 可以通过枚举类型 ElementType 的10个常量对象来指定
- TYPE,METHOD,CONSTRUCTOR,PACKAGE.....
@Retention:用于描述注解的生命周期- 可以通过枚举类型 RetentionPolicy 的3个常量对象来指定
- SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
- 唯有 RUNTIME 阶段才能被反射读取到。
@Documented:表明这个注解应该被 javadoc 工具记录。@Inherited:允许子类继承父类中的注解
自定义注解
一个完整的注解应该包含三个部分:声明、使用、读取。
【元注解】
【修饰符】 @interface 注解名{
【成员列表】
}- 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
- Annotation 的成员在 Annotation 定义中,以无参数、有返回值的抽象方法的形式来声明,我们又称为配置参数。
- 返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组
- 可以使用 default 关键字,为抽象方法指定默认返回值
- 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”。
- 如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value。
我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。
关键字的使用
package、import
package,称为包,用于指明该文件中定义的类、接口等结构所在的包。
语法格式:
package 顶层包名.子包名;包的作用:
- 包可以包含类和子包,划分项目层次,便于管理
- 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式
- 解决类命名冲突的问题
- 控制访问权限
JDK中主要的包介绍:
java.lang----包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能java.net----包含执行与网络相关的操作的类和接口。java.io----包含能提供多种输入/输出功能的类。java.util----包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。java.text----包含了一些java格式化相关的类java.sql----包含了java进行JDBC数据库编程的相关类/接口- java.awt(几乎用不到了) ----包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
[!info] MVC
MVC 是一种软件构件模式,目的是为了降低程序开发中代码业务的耦合度。
MVC设计模式将整个程序分为三个层次: 视图(Viewer)层 , 控制器(Controller)层 ,与数据模型(Model)层 。这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

this
我们在声明一个属性对应的setXxx方法时,通过形参给对应的属性赋值。如果形参名和属性名同名了,那么该如何在方法内区分这两个变量呢?
解决方案:使用this。具体来讲,this 关键字在属性、方法、构造器中都能使用,不过通常情况下是省略的,因为没有歧义。
- 使用 this 修饰的变量,表示的是属性(成员变量)。
- 没有用 this 修饰的变量,表示的是形参(局部变量)。
- 在方法(准确的说是实例方法或非static的方法)中调用时,this 表示的是当前对象。
- 在构造器中调用时,this 表示的是当前正在创建的对象。
在继承中,使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。
我的理解是本类从父类中继承了所有的属性和方法,只是由于封装性,本类可能没有某些属性/方法的使用权限。但是这些父类的属性、方法确实存在于子类中。
this可以作为一个类中构造器相互调用的特殊格式。这种相互调用,要注意规避隠式无限递归。
- this():调用本类的无参构造器。
- this(实参列表):调用本类的有参构造器。
- this()和this(实参列表) 都只能声明在构造器首行。
// 在构造器中调用 this
public User() {}
public User(String name) {
this();
this.name = name;
}
public User(String name, int age) {
this(name);
this.age = age;
}super
super 可以调用的结构:属性、方法;构造器。
- super调用父类的属性、方法
- 如果子父类中出现了同名的属性,此时使用super.的方式,表明调用的是父类中声明的属性。
- 子类重写了父类的方法。如果子类的任何一个方法中需要调用父类被重写的方法时,需要使用super.
- super调用构造器
- 在子类的构造器中,首行要么使用了"this(形参列表)",要么使用了"super(形参列表)"。
注意:
- 当子类、父类出现同名成员时,可以用super表明调用的是父类中的成员
- super 和 this 的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
super的追溯不仅限于直接父类。
子类中调用父类被重写的方法
只要权限修饰符允许,在子类中完全可以直接调用父类的方法。
- 方法前面没有super.和this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
- 方法前面有this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
- 方法前面有super.
- 从当前子类的直接父类找,如果没有,继续往上追溯
子类中调用父类中同名的成员变量
class Father {
int a = 10;
int b = 11;
}
class Son extends Father {
int a = 20;
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
}
}子类构造器中调用父类构造器
- 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。
- 规定:“super(形参列表)”,必须声明在构造器的首行。
- 前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器,结合第2条,得到结论:在构造器的首行,"this(形参列表)" 和 "super(形参列表)"只能二选一。
- 如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)", 则子类此构造器默认调用"super()",即调用父类中空参的构造器。
- 由3、4得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。 只能是这两种情况之一。
- 由5得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)",则剩下的那个一定使用"super(形参列表)"。
--> 我们在通过子类的构造器创建对象时,一定在调用子类构造器的过程中,直接或间接的调用到父类的构造器。也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法加载到内存中,供子类对象使用。
问题:在创建子类对象的过程中,一定会调用父类中的构造器吗? yes!
class A {
A() {
System.out.println("A类无参构造器");
}
A(int a) {
System.out.println("A类有参构造器");
}
}
class B extends A {
B() {
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a) {
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8 {
public static void main(String[] args) {
B b1 = new B();
B b2 = new B(10);
}
}开发中常见错误:如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错。
class A {
A(int a) {
System.out.println("A类有参构造器");
}
}
class B extends A {
B() {
System.out.println("B类无参构造器");
}
}
class Test05 {
public static void main(String[] args) {
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}static
static 用来修饰的结构:属性、方法; 代码块、内部类。
静态变量
如果想让一个成员变量被类的所有实例所共享,就用static修饰即可。
使用static修饰的成员变量就是静态变量(或类变量、类属性)。
[修饰符] class 类 {
[其他修饰符] static 数据类型 变量名;
}- 静态变量的默认值规则和实例变量一样。
- 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。
- 如果权限修饰符允许,在其他类中可以通过“
类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。 - 静态变量的get/set方法也静态的,当局部变量与静态变量
重名时,使用“类名.静态变量”进行区分。
对比静态变量与实例变量:
- 个数
- 静态变量:在内存空间中只有一份,被类的多个对象所共享。
- 实例变量:类的每一个实例(或对象)都保存着一份实例变量。
- 内存位置
- 静态变量:jdk6及之前:存放在方法区。 jdk7及之后:存放在堆空间
- 实例变量:存放在堆空间的对象实体中。
- 加载时机
- 静态变量:随着类的加载而加载,由于类只会加载一次,所以静态变量也只有一份。
- 实例变量:随着对象的创建而加载。每个对象拥有一份实例变量。
- 调用者
- 静态变量:可以被类直接调用,也可以使用对象调用。
- 实例变量:只能使用对象进行调用。
- 判断是否可以调用 ---> 从生命周期的角度解释
| 类变量 | 实例变量 | |
|---|---|---|
| 类 | yes | no |
| 对象 | yes | yes |
- 消亡时机
- 静态变量:随着类的卸载而消亡
- 实例变量:随着对象的消亡而消亡
[!info] 复习:变量的分类
方式1:按照数据类型:基本数据类型、引用数据类型
方式2:按照类中声明的位置:
- 成员变量:按照是否使用static修饰进行分类:
- 使用static修饰的成员变量:静态变量、类变量
- 不使用static修饰的成员变量:非静态变量、实例变量
- 局部变量:方法内、方法形参、构造器内、构造器形参、代码块内等。
静态方法
在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。
用static修饰的成员方法就是静态方法。
[修饰符] class 类 {
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}- 随着类的加载而加载
- 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
- 只要权限修饰符允许,静态方法在其他类中可以通过“
类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。 - 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
- 静态方法可以被子类继承,但不能被子类重写。
- 静态方法的调用都只看编译时类型,不存在运行时多态。
- 因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。如果有重名问题,使用“类名.”进行区别。
// 静态方法的调用都只看编译时类型。
public class TestStaticMethod {
public static void main(String[] args) {
Father.method();
Son.method(); //继承静态方法
Father f = new Son();
f.method(); //执行Father类中的method,而不是多态下调用Son类中的method
}
}final
final修饰类,表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。
- 例如:String类、System类、StringBuffer类
final修饰方法,表示这个方法不能被子类重写。
- 例如:Object类中的getClass()
final修饰变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量。
- “赋值”的方式包括:可以显式赋值、初始化块赋值。实例变量还可以在构造器中赋值
- 例如:final double MY_PI = 3.14;
- final与static搭配:修饰成员变量时,此成员变量称为:全局常量。比如:Math的PI。
[!info] 有哪些位置可以给成员变量赋值?
- 显式赋值
- 代码块中赋值
- 构造器中赋值
[!info] 有哪些位置可以给局部变量赋值?
- 方法内声明的局部变量:在调用局部变量前,一定需要赋值。而且一旦赋值,就不可更改
- 方法的形参:在调用此方法时,给形参进行赋值。而且一旦赋值,就不可更改
abstract
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。
有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
我们声明一些几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。
但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类。
abstract 使用说明:
- 抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。没有重写全部抽象方法的类,仍为抽象类。
- 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
- 理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。
注意事项
- 不能用abstract修饰变量、代码块、构造器;抽象类和抽象方法。
- 不能用abstract修饰私有方法、静态方法、final方法、final类。
- private 或 final修饰的方法不能被子类继承;
- 静态方法不能被重写,只能被隐藏。
- 抽象方法:通过对象实例调用,运行时多态。静态方法:通过类名调用,编译时绑定。
inferface、implements
enum
Object类的方法
Object类的全称是 java.lang.Object ,任何一个Java类(除Object类)都直接或间接的继承于Object类,因此Object类称为java类的根父类
- Object类中没有声明属性
- Object类提供了一个空参的构造器
- 常用方法:
- 重点方法:equals() \ toString()
- 了解方法:clone() \ finalize()
- 目前不需要关注:getClass() \ hashCode() \ notify() \ notifyAll() \ wait() \ wait(xx) \ wait(xx,yy)
equals()
java.lang.Object类中equals()的定义:
public boolean equals(Object obj) {
return (this == obj);
}==既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址equals属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
具体要看自定义类里有没有重写Object的equals方法来判断。
通常情况下,重写equals方法,会比较类中的相应属性是否都相等。
toString()
方法签名:public String toString()
Object类中toString()的定义:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}在进行String与其它类型数据的连接操作时,自动调用toString()方法
Date now = new Date();
System.out.println("now = " + now); //相当于
System.out.println("now = " + now.toString());如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()。
因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,
但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,
所以当你打印对象时,JVM帮你调用了对象的toString()。
可以根据需要在用户自定义类型中重写toString()方法,如String 类重写了toString()方法,返回字符串的值。
s1 = "hello";
System.out.println(s1); //相当于System.out.println(s1.toString());getClass()
public final Class<?> getClass():获取对象的运行时类型。
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}hashCode()
public int hashCode():返回每个对象的hash值。
知识补充
JavaBean的理解
JavaBean是一种Java语言写成的可重用组件。所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
用户可以使用JavaBean,将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。
用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。
设计模式
单例设计模式
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱"套路"。
经典的设计模式共有23种。每个设计模式均是特定环境下特定问题的处理方法。
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
实现思路:
如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。
单例模式的两种实现方式
// 饿汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}
// 懒汉式
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}
饿汉式:
- 特点:
立即加载,即在使用类的时候已经将对象创建完毕。 - 优点:实现起来
简单;没有多线程安全问题。 - 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会
耗费内存。
懒汉式:
- 特点:
延迟加载,即在调用静态方法时实例才被创建。 - 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会
节约内存。 - 缺点:在多线程环境中,这种实现方法是完全错误的,
线程不安全(放到多线程章节解决),根本不能保证单例的唯一性。
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
应用场景
- Windows的Task Manager (任务管理器)就是很典型的单例模式
- Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- Application 也是单例的典型应用
- 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
模板方法设计模式
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
当功能内部一部分实现是确定的,另一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。
JUnit单元测试
JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework),供Java开发人员编写单元测试之用。
JUnit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。
要使用JUnit,必须在项目的编译路径中引入JUnit的库,即相关的.class文件组成的jar包。jar就是一个压缩包,压缩包都是开发好的第三方(Oracle公司第一方,我们自己第二方,其他都是第三方)工具类,都是以class文件形式存在的。
(重点关注)要想能正确的编写单元测试方法,需要满足:
- 所在的类必须是 public 的,非抽象的,包含唯一的无参构造器。
- @Test标记的方法本身必须是 public,非抽象的,非静态的,void 无返回值,()无参数的。
设置执行JUnit用例时支持控制台输入。默认情况下,在单元测试方法中使用Scanner时,并不能实现控制台数据的输入。需要做如下设置:
- 在
idea64.exe.vmoptions文件(帮助-编辑自定义虚拟机选项)中加入下面一行设置,重启idea后生效。
-Deditable.java.test.console=true包装类
为了使得基本数据类型的变量具备引用数据类型变量的相关特征(比如:封装性、继承性、多态性),我们给各个基本数据类型的变量都提供了对应的包装类。
Java针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。
| 基本数据类型 | 包装类 | 缓存对象 |
|---|---|---|
| byte | Byte | -128~127 |
| short | Short | -128~127 |
| int | Integer | -128~127 |
| long | Long | -128~127 |
| float | Float | 没有 |
| double | Double | 没有 |
| boolean | Boolean | 0~127 |
| char | Character | true和false |
其中,前六个包装类的父类是 Number。
包装类与基本数据类型间的转换:装箱与拆箱。
- 装箱:把基本数据类型转为包装类对象。
- 转为包装类的对象,是为了使用专门为对象设计的API和特性。
- 拆箱:把包装类对象拆为基本数据类型。
- 转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。
// 基本数值---->包装对象
Integer obj1 = new Integer(4); //使用构造函数函数
Float f = new Float("4.56");
Integer obj2 = Integer.valueOf(4); //使用包装类中的valueOf方法
// Long l = new Long("asdf"); //NumberFormatException// 包装对象---->基本数值
Integer obj = new Integer(4);
int num1 = obj.intValue();自动装箱与拆箱:
由于我们经常要做基本类型与包装类之间的转换,从 JDK5.0 开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:
Integer i = 4; //自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5; //等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。
基本数据类型、包装类与字符串间的转换

// 基本数据类型 --> String类
String str1 = String.valueOf(3.4f);
String str2 = 23.4 + "";
// String类 --> 基本数据类型
// 方式1:除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型
int a = Integer.parseInt("123");
double d = Double.parseDouble("12.3");
boolean b = Boolean.parseBoolean("true");
// 方式2:通过包装类的构造器实现
int i = new Integer(“12”);
// 方式3:字符串转为包装类,然后可以自动拆箱为基本数据类型
int a = Integer.valueOf("123");
double d = Double.valueOf("1.23");
boolean b = Boolean.valueOf("false");包装类对象的特点
- 包装类缓存对象
Integer a = 1;
Integer b = 1;
System.out.println(a == b);//true
Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false
Integer m = new Integer(1);//新new的在堆中
Integer n = 1; //这个用的是缓冲的常量对象,在方法区
System.out.println(m == n);//false
Integer x = new Integer(1);//新new的在堆中
Integer y = new Integer(1);//另一个新new的在堆中
System.out.println(x == y);//false- 类型转换问题
Integer i = 1000;
double j = 1000;
System.out.println(i == j);//true 会先将i自动拆箱为int,然后根据基本数据类型“自动类型转换”规则,转为double比较
Integer i = 1000;
int j = 1000;
System.out.println(i == j);//true 会自动拆箱,按照基本数据类型进行比较
Integer i = 1;
Double d = 1.0
System.out.println(i == d);//编译报错- 包装类对象不可变
public class TestExam {
public static void main(String[] args) {
Integer j = new Integer(2);
change(j);
System.out.println("j = " + j);//2
}
/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类Integer等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(Integer b){
b += 10;//等价于 b = new Integer(b+10);
}
}