Java 面向对象笔记(下)
1.构造器
构造器是一个特殊的方法,这个方法能够创建类的实例。由于在Java 语言中构造器是创建对象的重要途径,所以在一个 Java 类中必须包含一个或多个构造器。
1.1.初始化构造器
构造器最大的用处就是在创建对象时执行初始化操作。因为构造器不是函数,所以它没有返回值。尽管构造器中可以存在 return 语句,但是 return 什么都不返回。
1.2.构造器重载
如果用户希望该类能保留无参数的构造器,或者希望有多重初始化方式,则可以为该类提供多个构造器。如果一个类里提供了多个构造器,那么就形成了构造器的重载。
一般我们为一个类编写有参构造,也要为该类额外编写一个无参构造。
public class AdminUser{
private int id;
private String username;
private String password;
//无参构造
public AdminUser(){}
//有参构造
public AdminUser(int id,String username,String password){
this.id = id;
this.username = username;
this.password = password;
}
}
2.多态
多态性是面向对象程序设计中一个重要的代码重用机制,它是面向对象中很普遍的一个概念。
多态是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的。
Java 实现多态有3 个必要条件:继承、重写和向上转型。只有满足这3 个条件,我们才能够在同一个继承结构中使用统一逻辑实现代码处理不同的对象,从而执行不同的行为。
- 继承:在多态中必须存在有继承关系的子类和父类
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
- 向上转型:在多态时需要将子类的引用赋给父类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
创建父类 Person类如下:
public class Person{
public String name;
public int age;
public Person(){}
public Person(String name,int age){
this.name = name;
this.age = age;
}
public void sayHello(){
System.out.println("Hello,I am a person");
}
}
创建子类 Student类如下:
public class Student{
private int sid;
public Student(){}
public Student(String name,int age,int sid){
super(name,age); //调用父类构造方法
this.sid = sid;
}
public void sayHello(){
System.out.println("Hello,I am a student");
}
}
创建子类 Teacher类如下:
public class Teacher{
private int tid;
public Teacher(){}
public Teacher(String name,int age,int tid){
super(name,age);
this.tid = tid;
}
public void sayHello(){
System.out.println("Hello,I am a teacher");
}
}
创建 Test 类,测试实现多态
public class Test{
public static void main(String[] args){
Person person; //声明 Person 类的变量
person = new Person();
person.sayHello();
person = new Student();
person.sayHello();
person = new Teacher();
person.sayHello();
}
}
以上代码,无论 person 变量的对象是 Student 还是 Teacher,它们都是 Person 类的子类,因此可以向上转型为该类,从而实现多态。
3.组合
继承是实现类重用的重要手段,但继承会破坏封装。相比之下,在 Java 中通过组合也可以实现类重用,且采用组合方式来实现类重用则能提供更好的封装性。
在继承关系中,子类可以直接获得父类的 public 方法。当程序在使用子类时,可以直接访问该子类从父类中继承的方法。而组合能够把旧类对象作为新类的属性来嵌入,用以实现新类的功能。我们看到的只是新类的方法,而不能看到嵌入在对象中的方法。因此,通常需要在新类里使用 private 来修饰嵌入的旧类对象。
下面使用继承方法来实现代码复用
class Person{ //父类
public void eat(){
System.out.println("go to eat");
}
}
//继承Person,Student类对象可以直接调用父类 Person类 的方法
public class Student extends Person{
public void sleep(){
System.out.println("go to sleep");
}
}
下面使用组合方法来实现代码复用
class Person{
public void eat(){
System.out.println("go to eat");
}
}
class Student{
// 将原来的父类嵌入原来的子类中,作为子类的一个组成成分
private Person person;
//重新定义自己的eat()方法
public void eat(){
// 直接复用 Person 类提供的eat()方法来实现 Student 类的eat()方法
person.eat();
}
}
组合 Vs 继承
- 组合是在组合类与被包含类之间的一种松耦合关系,而继承则是父类和子类之间的一种紧耦合关系
- 当选择使用组合关系时,在组合类中包含了外部类的对象,组合类可以根据自身需求,调用外部类相关方法,而使用继承关系时,子类无条件继承父类的所有属性与方法(构造方法除外),并无选择权
- 组合类不便于修改,它仅仅简单的调用了被包含类的相关接口,而继承除了复用其父类的接口外,同样具有重写功能,可以覆盖这些接口,从而轻松修改父类默认实现,这个特征也是组合关系所不具备的
- 从逻辑上来说,组合关系实现的仅仅是整体与部分的一种思想。而继承则是一种可回溯关系,子类代表父类的对象。
- 通俗理解:“组合”我雇了一个保姆打理家务,“继承”我的父亲在帮我打理家务
- 两者之间有什么的关系使用组合,而两者之间是什么样的关系使用继承
4.初始化块
Java 使用构造器对单个对象进行初始化操作,在使用构造器时需要初始化完成整个 Java 对象的状态,然后将 Java 对象返回给程序,从而让该 Java 对象的信息更加完整。在 Java 中,还有一个语言元素与构造器的功能很类似,那就是初始化块,它能够对 Java 对象实现初始化操作。
4.1.初始化块概述
在 Java 语言的类中,初始化块和属性、方法、构造器处于平等的地位。在一个类中可以有多个初始化块,在相同类型的初始化块之间是有顺序的,其中前面定义的初始化块先执行,后面定义的后执行。语法如下:
修饰符{
//初始化块的可执行代码
}
在 Java 语言中有两种初始化块,分别是静态初始化块和非静态初始化块。
静态初始化块使用static
定义,当类装载到系统时执行一次。如果在静态初始化块中向初始化变量,则只能初始化类变量,即由 static 修饰的数据成员。
非静态初始化块在生成每个对象时都会执行一次,可以初始化类的实例变量。非静态初始化块会在其构造器的主体代码之前执行。
public class AdminUser{
private int id;
private String username;
private String password;
//定义一个非静态初始化块
{
System.out.println("HelloWorld");
}
//定义无参构造
public AdminUser(){}
}
当创建Java 对象时,系统总是先调用在该类中定义的初始化块。如果在一个类里定义了两个普通的初始化块,则前面定义的初始化块后执行。
当 Java 程序创建一个对象时,系统先为该对象的所有实例属性分配内存,然后程序开始对这些实例属性执行初始化操作,初始化顺序是先执行初始化块或声明属性时指定的初始值,然后执行构造器里指定的初始值。初始化块虽然也是 Java 类的一种成员,但是因为它没有名字和标识,所以无法通过类和对象来调用初始化块。只有在创建 Java 对象时才能隐式地执行初始化块,并且应在执行构造器之前执行。
4.2.静态初始化块
非静态初始化块是在创建对象的时候才执行的。然而,静态初始化块是类相关的,在类初始化阶段就执行。因此,静态初始化块总是比普通初始化块先执行。
静态初始化块能够初始化整个类。它通常用于对类属性执行初始化处理,但是不能初始化实例属性。
class Department{
private int departmentId;
private String departmentName;
}
public class Employee{
private int id;
private String lastname;
private int age;
private static Department department; //员工所属部门
//定义静态初始化块
static{
this.department = new Department(0001,"人事部");
}
public E
}
5.final 修饰符
通过final 修饰,我们能够将其修饰的类、方法和变量表示成不可改变的状态。
5.1.用final 修饰变量
如果某个变量使用了 final 修饰符,那么就表示该变量一旦获得了初始值之后就不可改变了。final 既可以用来修饰成员变量,也可用来修饰局部变量和形参。
用 final 修饰成员变量
成员变量是随着类初始化或对象初始化而初始化的。当初始化类时,系统会为该类的类属性分配内存,并分配一个默认值;当创建对象时,系统会为该对象的实例属性分配内存,并分配默认值。
final 在修饰类属性、实例属性时能够指定初始值的一定范围,具体说明如下:
- 修饰类属性时,可在静态初始化块中生命该属性时指定初始值
- 修饰实例属性时,可在非静态初始化块声明该属性、构造器时指定初始值
用 final 修饰局部变量
在初始化局部变量时,局部变量必须由程序员显示初始化。因此使用 final 修饰局部变量时既可以指定默认值,也可以不指定默认值。如果在定义修饰的局部变量时没有指定默认值,则可以在后面代码中对该变量赋一个初始值,前提是这样的赋值操作只能执行一次,不能重复。如果在定义由 final 修饰的局部变量时已经指定了默认值,则在后面的代码中不能再对该变量赋值。
5.2.final 方法
在 Java 中,我们可以用 final 来修饰那些不希望重写的方法。譬如说,Java 的 Object 类中就有一个 final 方法——getClass(),因为 Java 不希望任何类重写这个方法,所以它的设计者就用 final 把这个方法封装起来。
6.内部类
内部类是指在外部类的内部再定义一个类。内部类作为外部类的一个成员,是依附于外部类而存在的。内部类可以是静态的,可以使用 protected 和 private 来修饰,而外部类只能使用 public 和 默认的包访问权限。
Java 中的内部类主要有 成员内部类、局部内部类、静态内部类和匿名内部类等。
6.1.内部类概述
在 Java 程序中,人们通常会把类定义成一个独立的程序单元。在某些情况下,我们也可以把类定义在另一个类的内部,这个定义在其他类内部的类称为内部类(有时也叫嵌套类),包含内部类的类称为外部类(有时也叫宿主类)。Java 从 JDK1.1 开始引入内部类。
我们为什么需要内部类?
典型的情况是,内部类继承某个类或实现某个接口,内部类的操作创建其外围类的对象。我们可以认为内部类提供了某种进入外围类的窗口。使用内部类最吸引人的原因是每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,这些对于内部类都没有影响。如果没有内部类提供的可以继承多个具体或抽象类的能力,则一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得更完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。
内部类的作用
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类的成员可以直接访问外部类的私有数据,因为内部类被当成了外部类的成员,同一个类的成员之间当然是可以互相访问的。但外部类不能访问内部类的实现细节,例如内部类的属性。
- 匿名内部类适合创建那些仅使用一次的类。
6.2.非静态内部类
成员内部类指在外部类的类体中定义的类,局部内部类指外部类的成员方法的方法体中定义的类。
**成员内部类包括静态内部类和非静态内部类。**使用 static 修饰的成员内部类是静态内部类,没有 static 修饰的是非静态内部类。因为内部类可以作为其外部类的成员,所以它可以使用任意访问控制符来修饰,例如 private 和 protected 等。非静态内部类也可以叫成员内部类。
定义非静态内部类的格式如下:
public class Outer{
class Inner{
//非静态内部类
}
}
public class Outer{
public int id = 1;
private String lastName = "zhang";
final int age = 18;
static String emailName = "zhangsan123";
public void setLastName(String lastName){
this.lastName = lastName;
}
public void getLastName(){return this.lastName;}
class Inner{ // 内部类可以访问外部类的所有成员,如果是有多层嵌套,内部类可以访问所有外部类的成员!!!
int tid = id + 1; // 访问外部类 public 的 id
String name = lastName + "san"; //访问 private 的lastName
int age2 = age + 3; // 访问 final 的 age
String email = emailName + "@qq.com"; // 访问static 的 emailName
String lastName2 = getLastName(); // 访问外部类的方法
}
}
总结来说,非静态内部类有以下特点:
- 在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例。
- 在非静态内部类中,可以访问外部类的所有成员。
- 在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。
- 外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例。
- 在实例内部类中不能定义 static 成员,除非同时使用 final 和 static 修饰。
6.3.静态内部类
非静态内部类在创建实例时需要外部类成员,即创建实例的语法如下:
Outer.Inner in = new Outer().new Inner();
普通的内部类(即非静态内部类)对象隐含的保存了一个引用,这个引用指向创建它的外围类对象,这也是为什么内部类可以访问外部类的所有成员的原因。
如果我们不需要内部类对象与其外部类对象之间有联系,就可以使用静态内部类。静态内部类有两个特点:
-
要创建内部类的对象不需要外部类成员,即创建实例的语法如下:
Outer.Inner in = new Outer.Inner();
-
不能从内部类的对象中访问非静态的外部类对象
public class Outer{
private int id = 1;
private static String lastName = "zhang";
static class Inner{
int tid = id + 1; // 会报错,Inner 不能访问不是 static 修饰的 Outer类属性
String name = lastName + "san"; // Inner 可以访问 static 修饰的 Outer类属性
}
}
6.4.局部内部类
在 Java 程序中,在方法中定义的内部类称为局部内部类。与局部变量类似,局部内部类不能有访问说明符,因为它不是外部类的一部分,但是它可以访问当前代码块内的常量和此外围类的所有成员。
public class Outer{
private String lastName = "zhang";
public static void main(String[] args){
int a = 100;
// 定义局部内部类
class Inner{
int b = a + 1; // 访问当前代码块内的常量
String name = lastName + "san"; // 可以访问此外部类的所有成员
}
}
}
7.匿名类
匿名类是指没有类名的内部类,必须在创建时使用 new 语句来声明类。
我们经常会在一些 Java 程序中看到一个很奇怪的写法,直接在代码中随机用 new 新建一个接口,然后在 new 里面粗暴地加入某些要执行的代码,就像下面的代码这样。
Runnable x = new Runnable(){// 直接新建接口
@Override
public void run(){ // 方法run() 的实现代码
System.out.println(this.getClass);
};
x.run(); // 调用方法run()
}
这里使用的就是匿名类,这样做的好处是使代码更加简洁、紧凑,模块化程度更高。
7.1.定义匿名类
在 Java 程序中,因为匿名类没有名字,所以它的创建方式有点儿奇怪,具体创建格式如下:
new <类或接口>(){
//匿名内部类的类体部分
}
在 Java 程序中的匿名类没有名称,所以不能在其他地方引用,也不能实例化,只能使用一次,当然它也就不能有构造器。
在 Java 程序中,匿名类根据存在位置不同分为两类:成员匿名类和局部匿名类。
public class AdminUser{
InterfaceA a = new InterfaceA(){}; // 成员匿名类
public static void main(String[] args){
InterfaceA a = new InterfaceA(){}; // 局部匿名类
}
//以上两种是通过接口实现匿名类的,称为接口式匿名类
//也可以通过继承类
AdminUser adminUser = new AdminUser(){};
}
7.2.匿名内部类
使用匿名内部类时必须要继承一个父类或者实现一个接口,当然也仅能继承一个父类或者实现一个接口。同时它也没有 class 关键字,这是因为匿名内部类是直接使用 new 来生成一个对象引用的,当然这个引用使隐式的。
//先定义一个抽象类或接口
abstract class Person{
private String name; //定义私有成员属性 name
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public abstract void study();
}
public class Student{
public void study(Person person){
person.study();
}
public static void main(String[] args){
Student s1 = new Student();
s1.study(new Person(){ // 直接使用匿名内部类来创建一个Person实例
public void study(){
System.out.println("I like study Java");
}
});
}
}
注意,匿名内部类存在一个缺陷,就是它仅能使用一次。在创建匿名内部类时会立即创建一个该类的实例,因为该类的定义会立即消失,所以匿名内部类不能够重复使用。如果我们需要对test() 方法里面的内部类使用多次,则建议重新定义一个类,而不是使用匿名内部类。
除此之外,当我们需要给匿名内部类传递参数的时候,并且如果会在内部类中使用该形参的话(比如类构造器),那么该形参就必须是由 final 修饰的。
8.枚举类
在大多数情况下,我们要实例化的类对象是有限而且固定的,例如季节类只要春、夏、秋、冬 4 个对象。这种实例数量有限而且固定的类,在 Java 里称为枚举类。
枚举类型从 JDK1.5 开始引入,Java 引入了一个全新的关键字 enum 来定义枚举类。使用方法如下:
public enum Season{
SPRING,SUMMER,AUTUMN,WINTER
}
enum 所定义的类型就是一个特殊的类。这些类都是类库中 Enum 类的子类(java.lang.Enum),它们继承了 Enum 中许多有用的方法。编译代码之后会发现,编译器将 enum 类型单独编译成了一个字节码文件 Season.class。
枚举类具有以下特征:
- 枚举类是一个不可以继承的 final 类。其枚举值(如 SPRING,SUMMER)都是 Season 类型的静态常量。枚举值都是 public static final 的,相当于常量,因此枚举类中的枚举值最好全部大写。
- 枚举类中同样可以有构造器、方法和数据域。但是 构造器只能为 private,绝对不允许有 public 构造器,这样可以保证外部代码无法构建新的枚举实例。同时构造器只在构造枚举值的时候调用。
- 所有枚举类都继承了 Enum 类的方法。
public enum Season{
// Season 类只能有以下这4个实例,不可改变
SPRING(1,3),SUMMER(4,6),AUTUMN(7,9),WINTER(10,12);
// 自定义数据域
private int beginMonth;
private int endMonth;
// 构造器构造枚举值
private Season(int beginMonth,int endMonth){
this.beginMonth = beginMonth;
this.endMonth = endMonth;
}
// 所有的枚举类都继承了 Enum 类的方法
public static void main(String[] args){
// 定义ordinal 方法返回枚举值在枚举类中的顺序,这个顺序根据枚举值声明的顺序而定
Season.SPRING.ordinal(); //返回结果:0
// 定义 compareTo 方法,返回两个枚举值的顺序之差
Season.SPRING.compareTo(Season.WINTER); //返回结果:-3
// 编写静态方法 values,返回一个包含全部枚举值的数组
Season[] seasons = Season.values();
for(Season s:seasons){ // foreach 遍历数组
System.out.println(c);
}
// 定义Season对象 s,并为它赋枚举常量值,然后返回对象 s 的枚举常量的名称
Season s = Season.SUMMER;
System.out.println(s); //返回结果:SUMMER
// 定义 valueOf 方法,此方法和 toString 方法是相对应的,返回带指定名称的指定类型的枚举常量
Season.valueOf("SPRING"); // 返回结果:Season.SPRING
}
// 定义 equals 方法比较两个枚举类对象的引用
public final boolean equals(Object other){
return this==other;
}
}