# Java 面对对象编程的概念
# 类
# 类的概念
类是描述世间万物的框架,在 java 中世间万物都可以用类来定义。
将数据及对数据的操作封装在一起,成为一个不可分割的整体。
同时将具有相同特征的对象抽象成一种新的数据类型 ---------- 类;
通过对象间的消息传递使整个系统运转,通过类的继承实现代码重用。
# 类的创建格式
[public] [修饰符] class [类名] extends (可选) [父类名] implements [接口 1 名],[接口 2 名],...{
// 类的成员
}
# 注意:
1.public 可选,当 java 文件名跟类名一致时,public 必须有
2. 第二个可选关键字有 final (子类不可继承)、abstract (无法实例化)
3. 第三个参数类名是定义的类名 (要符合 Java 类名命名规范)
# 类的初始化
【类名】 对象名 = new 【类名】();
【类名】 对象名 = new 【类名】(参数一...);
# 类的组成
类 = 字段 + 方法;
- 字段(属性)
描述一类对象的特征值。比如,人拥有姓名、性别、年龄等特征。
- 方法
描述一类对象的行为。比如,人会说话,会学习,会唱歌等行为。
# 对象
对象是类的实例。在 Java 中万物皆可看成是对象
# 类与对象的区别
类是同等对象的集合与抽象。它是一块创建现实对象的模板。对象是类的实例,对象是面向对象编程的核心部分,是实际存在的具体实体,具有明确定义的状态和行为。
# 例子
class Student{ | |
private int age ;// 字段 | |
private String name ; | |
private String sex ; | |
// 方法 | |
public void study(){ | |
System.out.println(name+"同学在学习"); | |
} | |
} | |
// 类 = 方法 + 字段 | |
public class Test{ | |
public static void main(String []args){ | |
Student student = new Student();// 声明类的对象 | |
} | |
} |
# 构造函数
构造函数是特殊的函数,函数名与类名一致,不能有返回值,包括 void;
声明格式为:[修饰符] 类名(参数列表){ //... } |
代码如下:
class Student{ | |
public Student(){ | |
} | |
} |
# 分类
# 无参构造
如果类中没有有参构造,也没有无参构造,则系统会默认给该类添加一个无参构造
class Teacher{ | |
Teacher(){} | |
} |
# 有参构造
如果没有无参构造,则有参构造函数则是初始化对象的构造函数(即必须传入参数才能完成初始化)
class Student{ | |
private String name ; | |
private int age ; | |
public Student(){} | |
public Student(String name,int age){ | |
this.name = name ; | |
this.age = age ; | |
} | |
} |
# 构造函数的重载
构造函数允许重载,可以根据需求重载构造函数。
public class Student{ | |
private String name ; | |
private int age ; | |
public Studnet(){ | |
} | |
public Student(String name){ | |
} | |
public Student(String name ,int age){ | |
} | |
} |
# 面对对象的三大特征
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象
封装的作用
封装把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象编程始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。封装是一种信息隐藏技术,在 java 中通过控制成员的访问权限实现封装,即使用方法将类的数据隐藏起来,控制用户对类的修改和访问数据的程度。 ** 适当的封装可以让代码更容易理解和维护,也加强了代码的安全性。继承
- 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法
- 一个新类可以从现有的类中派生,这个过程称为类继承,新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)
- 派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要
class Person{
}
class Student extends Person{
}
class Teacher extends Person{
}
多态
- 多态性是指允许不同类的对象对同一消息作出响应
- 多态性语言具有灵活、抽象、行为共享、代码共享的优势
# static 关键字
static
修饰符只能修饰类的成员
- 特点
static
只能修饰类成员(字段,方法), 另外构造函数不允许static
修饰static
修饰的类成员是属于所有类对象,这些成员所有对象共享 (即所有对象的该成员都在同一块内存区域)static
在类初始化前就已经加载完成了,所以它不能使用对象级别的其他成员staitc
表示静态成员,而静态成员只能使用静态成员 (无法在类中直接调用类的普通成员)
# final 关键字
final
在 Java 的原意是不可变,很多场合下都会和 static
一起使用,表示静态不可变成员
final
修饰的字段称为常量;常量在声明的时候就必须初始化完;常量一经确定无法更改;final
修饰的方法无法被重写final
修饰的类无法被继承 (如String
类、LocalDate
类等等)
class Student(){ | |
private final String name ; | |
private int age ; | |
public Student(){ | |
} | |
public Student(String name ,int age){ | |
this.name = name ; | |
this.age = age ; | |
} | |
} |
# 抽象类
抽象类对多态的实现
- 接口
- 抽象类
- 重写父类方法
抽象类的特点
- 抽象类中没有实例,即不能声明调用自己的构造方法
- 抽象类中可以有普通方法,抽象方法不能在抽象类中实现
- 继承了抽象类的子类必须实现该抽象类中所有的抽象方法,如果不实现,子类也必须定义为抽象类
# 接口
接口对多态的实现
接口的特征
- 接口没有构造方法,也不能实例化。
- 接口中的抽象方法和默认为公开的,变量默认为公开静态常量(建议不要再写这些修饰符)
- 接口中允许静态方法和默认 (
default
) 方法(JDK1.8) - 接口可以多继承(只允许接口之间)
interface Action{
void go() ;
int a = 100 ;
/**
* 下面的方法写法是在 JDK1.8 开始有的
*/
default void show(){
}
public static void see(){
}
}
# 对象转型
向上转型
父类对象或者实现类对象使用子类的引用
特点:
- 使用子类或实现类引用的父类对象或接口对象无法调用子类或实现类的方法,只能调用父类或接口中的方法和字段。
- 编译时引用是父类对象或接口对象,运行时引用是子类对象或实现类对象
class Person{
}
interface Action{
void act();
}
class Student extends Person implements Action{
}
class Test{
public static void main(String args[]){
Person person = new Student();// 父类对象使用子类的引用
Action action = new Student();// 接口对象使用子类的引用
}
}
# 抽象类和接口的区别
- 抽象类对属性没有限制,而接口的属性只能是公开静态常量
- 抽象类和接口都不能实例化,但是抽象类可以有构造方法 (仅供子类使用),而接口不能有构造方法
- 抽象类可以有普通方法,但接口不能有普通方法,接口只能有静态方法 (JDK8) 和默认方法 (JDK8)
- 抽象类只能单继承,而接口之间可以多继承 (只能接口之间)
# 内部类
什么是内部类?
内部类就是嵌套在类或者方法代码块内部的类
内部类的作用
我们为什么需要内部类?或者说内部类为啥要存在?其主要原因有如下几点:
内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
为什么内部类可以无条件地访问外围类的所有元素?
解答:
为什么可以引用?:
内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的 class 文件,内部类通过 this 访问外部类的成员。
- 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象 this 的引用;
- 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
- 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
编译指令 javac classpath (.java 文件的路径)
反编译指令 javap -v (详细信息) classpath (.class 文件的路径)class Outer{
private int a ;
private int b ;
class Inner{
private int a ;
private int b ;
public void print(){
System.out.println(a);
System.out.println(this.a);
System.out.println(Outer.this.a);// 通过 外部类名.this. 字段名 来访问外部重名字段
}
}
}
内部类可以对同一包中的其他类隐藏起来
实现隐藏
关于内部类的第二个好处其实很显而易见,我们都知道外部类即普通的类不能使用
private
,protected
访问权限符来修饰的,而内部类则可以使用private
和protected
来修饰。当我们使用private
来修饰内部类的时候这个类就对外隐藏了。这看起来没什么作用,但是当内部类实现某个接口的时候,在进行向上转型,对外部来说,就完全隐藏了接口的实现了- 内部类可以解决 java 单继承的缺陷
当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现
我们知道 java 是不允许使用extends
去继承多个类的。内部类的引入可以很好的解决这个事情。
我的理解 Java 只能继承一个类这个学过基本语法的人都知道,而在有内部类之前它的多重继承方式是用接口来实现的。但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法。
- 内部类可以解决 java 单继承的缺陷
内部类和外部类的关系
- 对于非静态内部类,内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的
- 内部类是一个相对独立的实体,与外部类不是 is-a (依赖) 关系
- 创建内部类的时刻 并不依赖于 外部类 的创建
内部类的分类
静态内部类
非静态内部类
非静态内部类访问权限的问题
非静态内部类和静态内部类的区别
静态内部类 非静态内部类 是否可以拥有静态成员 是 否 是否可以访问外部类的静态成员 是 是 是否可以访问外部类的非静态成员 否 是 创建是否依赖外部类 否 是
匿名内部类 (属于非静态内部类)
在定义时,要么给出类的超类,要么给出类要实现的接口(只能有一个);对外部类的访问权限同本地内部类相同;常见的用途是在建立 GUI 应用程序时为组件添加事件监听器对象
匿名内部类是没有访问修饰符的。
匿名内部类必须继承一个抽象类或者实现一个接口
匿名内部类中不能存在任何静态成员或方法
匿名内部类是没有构造方法的,因为它没有类名。
与局部内部类相同匿名内部类也可以引用局部变量。此变量也必须声明为
final
为什么局部变量需要 final 修饰呢?
因为局部变量和匿名内部类的生命周期不同。
匿名内部类是创建后是存储在堆中的,而方法中的局部变量是存储在 Java 栈中,当方法执行完毕后,就进行退栈,同时局部变量也会消失。
那么此时匿名内部类还有可能在堆中存储着,那么匿名内部类要到哪里去找这个局部变量呢?
为了解决这个问题编译器为自动地帮我们在匿名内部类中创建了一个局部变量的备份,也就是说即使方法执结束,匿名内部类中还有一个备份,自然就不怕找不到了。
但是问题又来了。
如果局部变量中的a
不停的在变化。那么岂不是也要让备份的a
变量无时无刻的变化。为了保持局部变量与匿名内部类中备份域保持一致。编译器不得不规定死这些局部域必须是常量,一旦赋值不能再发生变化了。
所以为什么匿名内部类应用外部方法的域必须是常量域的原因所在了。
特别注意
在 Java8 中已经去掉要对 final 的修饰限制,但其实只要在匿名内部类使用了,该变量还是会自动变为 final 类型(只能使用,不能赋值)。
匿名类的创建示例:
public class NickClass {
public static void main(String[] args) {
new Outer(){
@Override
void play() {
System.out.println("匿名内部类的抽象类无参构造创建方式");
}
}.play();
new Outer1(10){
void play(){
System.out.println("匿名内部类的抽象有参构造创建方式"+i);
}
}.play();
new Outer2(){
// 这里的接口匿名类创建方式,重写时必须将 public 修饰符加上,否则报错
public void play(){
System.out.println("匿名内部类的接口创建方式"+i);// 匿名内部类中可以访问原
}
}.play();
}
}
abstract class Outer{
abstract void play();
}
abstract class Outer1{
int i ;
public Outer1(int i){
this.i = i ;
}
public int getI(){
return i ;
}
abstract void play();
}
interface Outer2{
int i = 100 ;
void play();
}
匿名类使用局部变量
public class NickClassTest {
public static void main(String[] args) {
final int i = 100 ;// 匿名类允许使用常量
int j = 10;// 匿名类可以使用局部变量,但是不能改变其值
new Listener(){
@Override
void listen(int i1) {
// j = j+ 1; 编译错误,不能改变局部变量的值
System.out.println("使用局部变量i="+i+",j="+j);
System.out.println("i1="+i1);
}
}.listen(j);// 这里可以将需要传入方法的实参传入
}
}
abstract class Listener{
abstract void listen(int i);
}
成员内部类 (属于非静态内部类)
本地内部类 (局部内部类)(属于非静态内部类)
如果一个内部类只在一个方法中使用到了,那么我们可以将这个类定义在方法内部,这种内部类被称为局部内部类。其作用域仅限于该方法。
局部内部类有两点值得我们注意的地方:
- 局部内类不允许使用访问权限修饰符
public
,private
,protected
均不允许 - 局部内部类对外完全隐藏,除了创建这个类的方法可以访问它其他的地方是不允许访问的。
- 局部内部类与成员内部类不同之处是他可以引用成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这句话并不准确,因为如果不是基本数据类型的时候,只是不允许修改引用指向的对象,而对象本身是可以被就修改的)
- 局部内类不允许使用访问权限修饰符
内部类可能造成的问题
内部类会造成程序的内存泄漏
相信做 Android 的朋友看到这个例子一定不会陌生,我们经常使用的 Handler 就无时无刻不给我们提示着这样的警告。我们先来看下内部类为什么会造成内存泄漏。
要想了解为啥内部类为什么会造成内存泄漏我们就必须了解 java 虚拟机的回收机制,但是我们这里不会详尽的介绍 java 的内存回收机制,我们只需要了解 java 的内存回收机制通过「可达性分析」来实现的。
即 java 虚拟机会通过内存回收机制来判定引用是否可达,如果不可达就会在某些时刻去回收这些引用。
那么内部类在什么情况下会造成内存泄漏的可能呢?
- 如果一个匿名内部类没有被任何引用持有,那么匿名内部类对象用完就有机会被回收。
- 如果内部类仅仅只是在外部类中被引用,当外部类的不再被引用时,外部类和内部类就可以都被 GC 回收。
- 如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被 GC 回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。
# Java 包
为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间
包的作用:
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用
- 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
- 包也提供了限定了访问权限的一个控制范围,拥有包访问权限的类才能访问某个包中的类
Java 使用包这种机制是为了防止命名冲突,访问控制,提供搜索和定位类、接口、枚举和注
解等,它把不同的 java 程序分类保存,更方便的被其他 java 程序调用
- 以下是一些 JDK 中的包:
- java.lang:打包基础的类
- java.io:包含输入输出功能的函数
- java.util:包含一些重要的工具类
- …
开发者可以自己把一组类等组合定义自己的包。而且在实际开发中这样做是值得提倡的,将
相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注解等是相关的 。
- 由于包创建了新的命名空间,所以不会跟其他包中的任何名字产生命名冲突。使用包这种机
制,更容易实现访问控制,并且让定位相关类更加简单。
# package 与 import 关键字
Java 中用 package 语句来将一个 Java 源文件中的类打成一个包
package 语句必须作为 Java 源文件的第一条语句,指明该文件中定义的类
所在的包。(若忽略该语句,则指定为无名包)。它的格式为:
package pkg1[.pkg2[.pkg3…]];
Java 编译器把包对应于文件系统的目录管理
package 语句中,用 “.” 来指明目录的层次
包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它
为了能够使用其他包的成员,需要在 Java 程序中明确导入该包
使用 "import" 语句可完成此功能
在 java 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为:
import package1[.package2…].(classname|); |
如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略
import 语句中类名部分可以使用通配符 “*”
符号 * 表示直接导入包中所有的类
如:
import java.util.*;
表示导入java.util
包中所有的类注意:包和子包之间不存在继承关系,只要两个类不直接在同一个文件中即认为位于不同的包,因此 * 号只能包含本包中的类而不能包含子包中的类
包的命名规则
创建包的时候,你需要为这个包取一个合适的名字,根据 Java 包的约定,名字内的所有字母都应小写,之后,如果非同包的其他的一个源文件使用了这个包提供的类、接口、枚举或者注释类型的时候,都必须在这个源文件的开头说明所引用的包名
通常,一个公司使用它互联网域名的颠倒形式来作为它的包名。例如:互联网域名是 chinasofti.com,所有的包名都以 com.chinasofti 开头
# 访问控制符
private
本类友好
public
所有友好
缺省的
同包类友好
protected
同包类友好,不同包的子类友好
访问权限示意图
本类 | 同包类 | 不同包子类 | 不同包类 | |
---|---|---|---|---|
private | √ | |||
缺省的 | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
# 枚举
Java
枚举( Enum
)是一种特殊的类,用于表示一组相关的常量。枚举常量是预定义的,不允许添加或删除。枚举在 Java
中是一种基本数据类型,可以单独定义或嵌套在类或接口中。
# 枚举示例
以下是一个枚举类的示例:
public enum DayOfWeek { | |
MONDAY, | |
TUESDAY, | |
WEDNESDAY, | |
THURSDAY, | |
FRIDAY, | |
SATURDAY, | |
SUNDAY; | |
} |
这个枚举类表示一周中的每一天,包括星期一至星期日。枚举常量(例如 MONDAY)是 DayOfWeek 类的实例,它们是 final、static 和 public 类型的。这意味着它们是不可修改的常量,可以在没有类实例的情况下访问。
枚举常量可以有属性和方法。以下是一个带有属性和方法的示例:
public enum Gender { | |
MALE("男"), | |
FEMALE("女"); | |
private String name; | |
Gender(String name) { | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
} |
这个枚举类表示性别,包括男和女。枚举常量 MALE 和 FEMALE 都有一个名为 name 的属性,它们在构造函数中被初始化。此外,Gender 类还有一个名为 getName 的方法,用于返回枚举常量的 name 属性。
枚举可以用于 switch 语句。以下是一个使用 switch 语句的示例:
public void printDay(DayOfWeek day) { | |
switch (day) { | |
case MONDAY: | |
System.out.println("星期一"); | |
break; | |
case TUESDAY: | |
System.out.println("星期二"); | |
break; | |
case WEDNESDAY: | |
System.out.println("星期三"); | |
break; | |
case THURSDAY: | |
System.out.println("星期四"); | |
break; | |
case FRIDAY: | |
System.out.println("星期五"); | |
break; | |
case SATURDAY: | |
System.out.println("星期六"); | |
break; | |
case SUNDAY: | |
System.out.println("星期日"); | |
break; | |
default: | |
System.out.println("无效的日期"); | |
break; | |
} | |
} |
这个方法使用 switch 语句打印给定日期的星期几。使用枚举作为 switch 语句的参数可以使代码更清晰易读。
枚举类也可以实现接口、继承其他类或枚举,或者被其他类继承。这使得枚举类更加灵活和可扩展。
- 为什么要有枚举?枚举的作用
Java
引入枚举是为了提高代码的可读性和可维护性。枚举类型在 Java
中表示一组固定的常量,可以在代码中使用这些常量,而不必担心拼写错误或者传递无效的参数。这样可以减少由于错误参数而引起的问题,同时也使代码更加清晰和易于维护。
枚举的主要用处包括:
- 限制变量的取值范围,提高代码的可读性和可维护性。
- 枚举常量在代码中使用时具有类型安全性,可以防止类型转换错误。
- 枚举常量可以拥有自己的属性和行为,类似于类的实例,可以实现更复杂的功能。
- 枚举常量可以作为参数传递给方法,可以提高代码的可读性和可维护性,同时也可以防止传递无效的参数。
- 枚举常量可以作为集合类型的元素,可以更方便地对集合进行操作。
总之, Java
中的枚举类型可以帮助程序员编写更可读、更可维护、更类型安全和更清晰的代码,提高程序的可靠性和可维护性。
- 枚举的使用
Java 枚举的使用可以分为以下几个方面:
- 定义枚举类型
Java 枚举类型通过关键字 enum
定义,语法如下:
enum EnumName { | |
ENUM_VALUE1, | |
ENUM_VALUE2, | |
ENUM_VALUE3, | |
// ... | |
} |
其中, EnumName
表示枚举类型的名称, ENUM_VALUE1
、 ENUM_VALUE2
、 ENUM_VALUE3
等表示枚举常量。
- 枚举常量的使用
枚举常量通过 EnumName.ENUM_VALUE
的方式访问,例如:
EnumName enumValue = EnumName.ENUM_VALUE1; |
- 枚举类型的方法
枚举类型可以定义自己的方法,例如:
enum EnumName { | |
ENUM_VALUE1, | |
ENUM_VALUE2, | |
ENUM_VALUE3; | |
public void myMethod() { | |
System.out.println("This is my method."); | |
} | |
} |
在枚举类型中,每个枚举常量都是一个实例对象,因此可以在枚举常量上调用枚举类型中的方法:
EnumName enumValue = EnumName.ENUM_VALUE1; | |
enumValue.myMethod(); // 输出 "This is my method." |
- 枚举类型的构造函数
枚举类型也可以有自己的构造函数,但是枚举常量必须在枚举类型定义的最开始处定义。例如:
enum EnumName { | |
ENUM_VALUE1("value1"), | |
ENUM_VALUE2("value2"), | |
ENUM_VALUE3("value3"); | |
private String value; | |
private EnumName(String value) { | |
this.value = value; | |
} | |
public String getValue() { | |
return value; | |
} | |
} |
在这个例子中,枚举类型 EnumName
有一个私有成员变量 value
,并且定义了一个有参数的构造函数,每个枚举常量都必须在定义时指定对应的参数。在枚举类型中,可以通过调用 getValue()
方法获取枚举常量对应的参数值。
- 枚举类型的比较
枚举类型可以通过 ==
符号进行比较,例如:
EnumName enumValue1 = EnumName.ENUM_VALUE1; | |
EnumName enumValue2 = EnumName.ENUM_VALUE1; | |
System.out.println(enumValue1 == enumValue2); // 输出 "true" |
以上是 Java 枚举的基本使用方法。