面向对象(下)

## 包装类

基本数据类型的数据不具备“对象”的特性,例如所有引用变量类型都继承了Object类,但基本数据类型却没有。

因此为了解决基本数据类型的变量不能当做Object类型变量使用的问题,Java提供了包装类

byte—>Byte, short—>Short, int—>Integer, long—>Long, char—>Character, float—>Float, double—>Double,

boolean—>Boolean

Java还提供了自动装箱和自动拆箱,自动装箱是把基本类型变量直接赋给对应包装类变量,不需要强制转换。

拆箱就是反过来。

除此之外,包装类提供了基本类型变量和字符串之间的转换,把字符串值转换成基本类型的值有两种方式

  • 利用包装类提供的**parseXxx(String s)**静态方法
  • 利用包装类提供的**valueOf(String s)**静态方法

String类也提供了多个重载valueOf()方法,把基本类型变量转换成字符串。

提示:如果把基本类型变量转换成字符串,有一种更简单方法,就是将基本类型变量与""相连接

注意:包装类的实现,例如Integer包装类,系统会创建一个cache的数组,把-128~127的整数放在数组中,缓存起来,当把一个在这个范围内的数自动装箱成一个实例时,实际指向的是数组对应的元素。

处理对象

Java对象都是Object类的实例,都可以直接调用该类的方法。

toString()是Object类的一个实例方法,它总是返回”类名@hashcode“值,当使用System.out.println(p)打印p对象时,打印的实际上是p对象toString的返回值。

这个返回值不能满足输出对象内容的功能,因此用户需要自定义类能实现这个功能,就要重写这个方法。


Java测试变量相等有两种方法,一个是用==运算符,一个是用equals方法。

当时用==来判断两个变量是否相等时,如果都是基本类型变量且都是数值类型,则只要两个变量值相等,返回true

但是如果是两个引用类型变量,则只有他们指向同一个对象时,即他们存放的地址值相等时,才会返回true。

使用equals判断,与使用==判断没有区别,仍然要求引用变量指向同一个对象才回返回true,因此如果要采用自定义的相等标准,需要重写equals方法。

注意:String类已经重写了euqals方法,可以直接判断两个字符串包含的字符序列是否相等。

重写equals方法需要满足几个条件

  • 自反性:对任意x,x.equals(x)一定返回true
  • 对称性:对任意x和y,如果x.equals(y)返回true,则y.equals(x)也必须返回true
  • 传递性:对任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也必须返回true
  • 一致性:对任意x和y,如果对象中判断相等的信息没有改变,则无论调用多少次equals方法,返回的结果应该保持一致
  • 对任意不是null的x,x.equals(null)一定返回false

关于常量池

JVM有一个常量池来管理在编译时被确定并被保存在已编译的.class文件中,它包括了类、方法、接口中的常量和字符串常量。

常量池分为两种:静态常量池和运行时常量池

静态常量池就是上述所说的,被保存在了class文件,当运行时,class文件被加载到内存中,就是运行时常量池。

当时用String p1 = "hello"和使用String p2 = new String("hello")时,前者会将字符串"hello"放在常量池中,让p1去引用,后者会先使用常量池管理"hello"直接量,然后调用String构造器创建一个新的String对象,它被保存在堆内存中。

需要注意的是,如果在上面两句代码后再加String p3 = "he" + "llo",则p3 == p1返回true,因为系统在编译期间执行了+号,将字符串拼接起来,并自动进行优化,所以还是引用常量池的字符串。

上面所说的包装类的实现也是一种常量池应用


类成员

用static修饰的成员就是类成员,类变量属于整个类,当系统第一次准备使用该类时,系统会为该类变量分配内存,类变量生效,直到该类被卸载。

类变量可以通过类来访问,也可以通过类的对象来访问。但是当类的对象访问类变量时,**其实系统会在底层转换成该类来访问类变量。当创建类的对象时,系统也不会再为类变量分配内存。**即对象根本不拥有对应类的类变量。

当一个类的对象为null时,如Person p;创建了一个Person类对应的对象,但是p还没有指向任何内存,即p = null,p依然可以直接访问类的类成员。

final修饰符

final用于修饰类、变量和方法。表明是不可变的。

final修饰变量时,表示该变量一旦获得初始值就不能再改变了,final可以修饰成员变量,也可以修饰局部变量和形参。

  • 当final修饰成员变量时,虽然系统会默认初始化,但必须由程序员另外显式指定初始值
  • final修饰局部变量时,系统不会默认初始化,也必须要显式指定初始值

final修饰引用类型变量时,引用类型变量本身不能改变,即它必须一直引用这个对象,但它所引用的对象是可以改变的。


对一个final变量来说,不管是类变量、成员变量还是局部变量,只要满足三个条件,这个final变量就不是一个变量,而是一个直接量或者常量。

  • 使用final修饰
  • 在定义该final变量时指定了初始值
  • 该初始值可以在编译时被确定下来

例如

1
2
3
4
5
6
public class Main{
public static void main(String args[]){
final int a = 5;
System.put.println(a);
}
}

对于这个程序来说,a变量不存在,当执行System.put.println(a)时,实际转换为执行System.put.println(5)。

即变成了一个宏变量。编译器会把所有用到该变量的地方都替换成对应的值。


final方法

final修饰的方法不可以被重写,但可以被重载。


final类

final修饰的类不能有子类,


不可变类

不可变类是指创建该类的实例后,该实例的实例变量是不可改变的。

如果要创建自定义的不可变类,要遵守几条规则

  • 使用private和final修饰符来修饰该类的成员变量
  • 提供带参数的构造器,用于根据传入参数来初始化类的成员变量
  • 仅为该类提供getter方法,不要为该类成员变量提供setter方法
  • 如果有必要,重写equals和hashcode方法

注意:如果定义一个不可变类,但这个不可变类有引用类型变量,final修饰引用类型变量,则变量引用的对象不能改变,但对象本身可以改变,因此一定要采用必要措施来保护引用类型对象不会被改变,这样才能创建真正的不可变类。


抽象类

抽象方法和抽象类使用abstract修饰符来修饰,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。

抽象类只能用public修饰符或者省略修饰符来修饰

几条规则

  • 抽象类和抽象方法必须使用abstract修饰,抽象方法不能有方法体
  • 抽象类不能被实例化,也无法使用new关键字来调用抽象类的构造器,即使抽象类不包含抽象方法
  • 抽象类里可以包含成员变量,方法(普通方法和抽象方法),构造器,初始化块,内部类(接口和枚举)五种成分,抽象类的构造器不能用于创建实例,主要用于被子类调用
  • 含有抽象方法的类(包括直接定义了一个抽象方法,或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或者实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义为抽象类

抽象类不能用于创建实例,只能当做父类被子类继承。

注意:如果抽象类里有抽象方法和普通方法,则它的子类必须实现抽象方法,普通方法可实现可不实现。

注意:abstract和static不能同时使用,但不是绝对的,它俩可以同时用来修饰内部类;abstract和final也不能同时使用;private和abstract也不能同时使用,因为abstract修饰的方法必须被子类重写才有意义。


接口

将抽象类进行的更彻底,可以提炼出一种更加特殊的“抽象类”——接口,接口可以定义默认方法、类方法和私有方法,都可以提供方法实现。

接口可以继承多个接口,但是不能继承类。

接口只能用public或者省略访问控制符来修饰。

接口不能包含构造器和初始化块定义,可以包含成员变量(只能是静态变量)、方法(只能是抽象实例方法、默认方法、类方法和私有方法)、内部类(内部接口、枚举)。


有无方法体是否需要被实现类重写默认修饰符
普通方法(抽象实例方法)必须被实现类重写public abstract
默认方法(带方法体实例方法)可以被实现类重写public default
类方法不可以public static
私有方法不可以private

注意

  • 系统会自自动为普通方法加上abstract修饰符
  • 默认方法必须使用的default修饰,不能使用static修饰
  • 私有方法主要作为工具方法,不能使用default修饰,可以是类方法或者实例方法

接口中定义的静态变量,不管是否使用public static final修饰符,系统都会为变量自动加上。
且可以在接口修饰的访问范围内,使用静态变量,直接用“接口名.变量名”来进行调用。

接口中定义的内部类。内部接口和内部枚举都会自动采用public static 修饰符。


一个接口如果继承了另一个接口,将会获得该接口的所有抽象方法和静态变量。

一个类如果实现了一个接口,必须实现该接口所有抽象方法,可以实现该接口默认方法,同时获得该接口的静态变量。


使用接口

接口不能用于创建实例,但可以声明引用类型变量,并且变量所引用的对象对应的类实现了该接口。

接口主要的用途就是被实现类实现。归纳来说,接口的用途有:

  • 定义变量,也可以用于强制类型转换
  • 调用接口中定义的常量
  • 被其他类实现

一个类可以实现多个接口,使用implements关键字,一旦实现接口,可以获得该接口的静态变量,普通方法和默认方法。

而且因为子类重写父类方法时,访问权限只能更大或者相等,所以实现类实现接口的方法只能使用public修饰。


接口和抽象类

接口和抽象类的相同点

  • 都不可以被实例化
  • 都位于继承树顶端,用于被其他类实现或继承
  • 都可以包含抽象方法,实现接口或继承抽象类的类都必须实现这些抽象方法

接口和抽象类的不同点

    • 接口体现的是一种规范,对于接口实现者来说,接口规定了实现者必须向外提供什么服务,对于接口调用者来说接口规定调用者可以使用哪些服务,某种程度上来说,接口类似整个系统的“总纲”,接口不应该经常被改变
    • 抽象类则体现了一种模块化设计,抽象类作为多个子类的抽象父类,可以当成系统实现的中间产品,这个中间产品已经实现了部分功能,但不能被当成最终产品,必须有进一步完善
    • 接口只能包含抽象方法、类方法、默认方法和私有方法,不能为普通的方法提供实现
    • 抽象类可以包含普通方法
    • 接口只能定义静态常量,不能定义普通成员变量
    • 抽象类既可以定义普通成员变量,也可以定义静态常量
    • 接口中不包含初始化块和构造器
    • 抽象类可以包含初始化块和构造器,构造器不是为了创建对象,而是让子类调用
    • 一个类可以实现多个接口
    • 一个类最多只有一个直接父类,包括抽象类

内部类

定义在其他类内部的类就是内部类,包含内部类的类是外部类。

内部类有几个作用

  • 内部类提供更好的封装,把内部类隐藏在外部类之内,同一个包的其他类无法访问
  • 内部类成员可以访问外部类的私有数据,因为内部类被当成外部类成员,同一个类的成员之间可以相互访问
  • 匿名内部类适用于创建只需要使用一次的类

同时,内部类可以比外部类多使用三个修饰符:private、protected、static,外部类不能使用这三个修饰符。

内部类主要分四类

  • 成员内部类:即在类里定义的,没有用static修饰的类
  • 静态内部类:在类里定义的,但使用static修饰的类
  • 局部内部类:定义在一个方法或者一个作用域里面的类
  • 匿名内部类:通过已有的接口或者类来创建,没有名字

成员内部类

成员内部类是最普通的内部类,被定义在外部类的内部

1
2
3
4
5
6
7
8
9
10
11
12
13
class Circle {
double radius = 0;

public Circle(double radius) {
this.radius = radius;
}

class Draw { //内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}

例如Draw就是一个成员内部类

特点:

  • 成员内部类可以无条件访问外部类的所有成员变量和方法
  • 成员内部类是依附于外部类的,因此如果要创建一个成员内部类对象,首先要创建一个外部类对象
  • 成员内部类不能定义静态变量或者方法

注意:

  • 外部类不能直接访问成员内部类的成员变量和方法,必须要创建一个内部类对象,通过这个对象才能进行访问。
  • 如果成员内部类里定义了和外部类重名的变量,会发生隐藏现象,即默认访问的是成员内部类定义的变量,如果要访问外部类的同名成员,需要用“外部类名.this.成员变量/方法”来使用

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Circle {
double radius = 0;

public Circle(double radius) {
this.radius = radius;
}

static class Draw { //静态内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}

使用static修饰的成员内部类就是静态内部类,可以把外部类当成静态内部类的包

特点

  • 静态内部类不需要依附于外部类的对象,可以直接用“外部类名.内部类名”的方式来创建静态内部类
  • 静态内部类可以包含静态成员和非静态成员
  • 静态内部类只能访问外部类的静态成员,不能访问非静态成员

除此之外,接口里也可以定义内部类,默认使用public static修饰符,也就是说,接口内部类只能是静态内部类


关于成员内部类和静态内部类访问问题

成员内部类可以访问外部类的静态成员和非静态成员,也可以访问它的外部类定义的其他静态内部类里的静态成员

外部类不能直接访问成员内部类的成员,只能通过创建成员内部类的对象来访问。

静态内部类只能访问外部类的静态成员,不能访问外部类的非静态成员

外部类可以访问静态内部类的静态成员,不能访问静态内部类的非静态成员

简单的来说,非静态的“东西”可以访问静态的和非静态的,但是静态的“东西”只能访问静态的


局部内部类

局部内部类非常简单,把一个类放在方法中或者一个作用域中定义,它就是局部内部类,在这个方法或者作用域之外,这个类不存在,也不能被访问,因此局部内部类不能使用访问控制符或者static修饰符修饰

实际开发很少使用局部内部类,因为它的作用域太小


## 匿名内部类

匿名内部类适合只需要使用一次的类,创建匿名内部类时会立即创建一个该类的实例,匿名内部类不能重复使用

定义匿名内部类的格式为

new 实现接口() | 父类构造器(实参列表) {匿名内部类的类体部分}

可以看出,匿名内部类必须继承一个父类或实现一个接口,但最多只能继承一个父类,或实现一个接口

还有两条规则

  • 匿名内部类不能是抽象类,因为系统创建匿名类时,会立即创建匿名内部类的对象
  • 匿名内部类不能定义构造器,因为匿名内部类没有类名,但可以有初始化块

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Product{
double getPrice();
String getName();
}
public class Main{
public void test(Product p){
System.out.println(p.getName + p.getPrice());
}

public static void main(String args[]){
Main a = new Main();
a.test(new Product(){
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP";
}
})
}
}

即在Main里定义了test方法,需要Product参数,但Product无法直接创建实例,需要考虑传入Product接口实现类的对象,则可创建一个匿名内部类。

匿名内部类必须实现它的抽象父类或者接口包含的所有抽象方法。

被匿名内部类访问的局部变量必须使用final修饰。


Lambda表达式

Lambda表达式就是用来创建只有一个抽象方法的接口的实例。

只有一个抽象方法的接口叫做函数式接口,但可以包含多个默认方法和类方法。

Lambda表达式的主要作用就是就是代替匿名内部类的繁琐语法,它由三部分组成

  • 形参列表:如果形参列表只有一个参数,甚至连形参列表的圆括号也可以省略
  • 箭头:->
  • 代码块:如果代码块只包含一条语句,可以省略代码块的花括号

例子:

1
2
3
4
ProcessArray pa = new ProcessArray();
pa.process(array, (int elemenmt)->{
System.out.println("数组元素的平方是:" + element * element);
});

上述代码中,pa的第二个参数是一个函数式接口的实现类,就可以使用Lambda表达式直接创建一个实现类。

Lambda表达式有两个限制

  • Lambda表达式的目标类型只能是明确的的函数式接口
  • Lambda表达式只能为函数式接口创建对象,Lambda表达式只能实现一个方法

方法引用和构造器引用

如果Lambda表达式的代码块只有一条代码,程序可以省略Lambda表达式的花括号,不仅如此,如果Lambda表达代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用

种类示例说明对应的表达式
引用类方法类名::类方法函数式接口被实现方法
全部参数传给该类方法
(a, b…)->类名::类方法(a, b, …)
引用特定对象实例方法特定对象::实例方法函数式接口被实现方法
全部参数传给该方法
(a, b…)->特定对象.实例方法(a, b…)
引用某类对象实例方法类名::实例方法接口第一个参数作为调用者
后面参数全部传给该方法
(a. b…)->a.实例方法(a,b…)
引用构造器类名::new接口全部参数传给该构造器(a, b…)->new 类名(a, b…)

定义一个函数式接口

1
2
3
4
@FunctionalInterface
interface Converter{
Integer convert(String from);
}
  1. 引用类方法

    Converter converter1 = from-> Integer.valueOf(from);

    可以写成:

    Converter converter1 = Integer::valueOf;

  2. 引用特定对象实例方法

    Converter converter2 = from ->"kit.org".indexOf(from);

    可以写成:

    Converter converter2 = "fkit.org::indexOf;"

  3. 引用某类对象的实例方法

    定义如下函数式接口

    1
    2
    3
    4
    @FunctionalInterface
    interface Mytest{
    String test(String a, int b, int c);
    }

    使用Lambda表达式创建对象

    Mytest mt = (a, b, c)-> a.substring(a, b);

    可以写成:

    Mytest mt = String::substring;

  4. 引用构造器

    定义如下函数式接口

    1
    2
    3
    4
    @FunctionalInterface
    interface Mytest{
    JFrame win(String title);
    }

    使用Lambda表达式创建一个对象

    Mytest mt = a -> new JFrame(a);

    可以写成:

    Mytest mt = JFrame::new;

匿名内部类和Lambda表达式的区别和联系

相似点:

  • 都可以直接访问“effectively final”的局部变量,以及外部类的成员变量
  • 它们创建的对象都可以直接调用从接口继承的默认方法

不同点:

  • 匿名内部类可以为任意接口创建实例,不管有多少抽象方法,但Lambda表达式只能为函数式接口创建实例
  • 匿名内部类可以为抽象类甚至普通类创建实例,但Lambda表达式不是
  • 匿名内部类实现的抽象方法的方法体允许调用接口定义的默认方法,但Lambda表达式不允许

枚举类

实例有限且固定的类称为枚举类

使用enum定义枚举类,他也可以有自己的成员变量、构造器和方法,可以实现接口

但枚举类和普通类有区别

  • 枚举类可以实现接口,使用enum定义的枚举类默认继承了java.lang.Enum类,不是默认继承Object类,因此枚举类不能显式继承其他的父类
  • 使用enum定义的非抽象的枚举类默认使用final修饰
  • 枚举类构造器只能使用private修饰,如果省略了访问控制符,系统会默认加上
  • 枚举类所有实例必须在枚举类第一行显式列出,否则这个枚举类永远不能产生实例,这些实例系统会自动添加public static final修饰,无需显式添加

枚举类提供一个values方法,可以遍历所有枚举值

例子

1
2
3
public enum SeasonEnum{
SPRING, SUMMER, FALL, WINTER;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class EnumTest
{
public void judge(SeasonEnum s)
{
// switch语句里的表达式可以是枚举值
switch (s)
{
case SPRING:
System.out.println("春暖花开,正好踏青");
break;
case SUMMER:
System.out.println("夏日炎炎,适合游泳");
break;
case FALL:
System.out.println("秋高气爽,进补及时");
break;
case WINTER:
System.out.println("冬日雪飘,围炉赏雪");
break;
}
}
public static void main(String[] args)
{
// 枚举类默认有一个values方法,返回该枚举类的所有实例
for (var s : SeasonEnum.values())
{
System.out.println(s);
}
// 使用枚举实例时,可通过EnumClass.variable形式来访问
new EnumTest().judge(SeasonEnum.SPRING);
}
}

switch括号的表达式可以是任何枚举类型,且后面case无需添加枚举类作为限定。

枚举类提供几种常用方法

  • int compareTo(E o):用于与指定枚举对象比较顺序,如果该枚举对象在指定枚举对象之后,返回正整数,之前负整数,否则返回0
  • String name():返回此枚举实例的名称
  • String toString():也是返回枚举实例名称,优先使用这个
  • int ordinal():返回枚举值在类中的索引值,就是声明索引中的位置,从零开始
  • public static <T extends Enum> T valueOf(Class, String name):静态方法,返回指定枚举类中制定名称的枚举值,名称必须与在该枚举类中声明时一致

枚举类成员变量、方法和构造器.

Java应该把所有类设计成良好封装的类,所以不应该允许直接访问枚举类的成员变量,应该把成员变量全部设为private final修饰。

例如一个枚举类

1
2
3
4
public enum Gender{
MALE, FEMALE;
public String name;
}

不应该允许直接访问Gender的name成员变量,如果出现

1
2
Gender g = Enum.valueOf(Gender.class, "FEMALE");
g.name = "男";

就会出现混乱。

如果每个成员用final修饰,就应该在构造器为成员变量指定初始值,例子如下

1
2
3
4
5
6
7
8
9
10
11
public enum Gender{
MALE("男"), FEMALE("女");
private final String name;
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}

}

在列出枚举值时直接调用构造器创建枚举类对象,这里无需使用new关键字

### 实现接口的枚举类

枚举类可以实现一个或多个接口

例如定义一个接口

1
2
3
4
public interface GenderDesc
{
void info();
}

然后可以使枚举类实现这个接口,但是如果直接实现info()方法时,那么每个枚举值在调用这个方法时都有相同行为方法,如果要让每个枚举值在调用这个方法时呈现不同的行为,可以让每个枚举值实现该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum Gender implements GenderDesc{
MALE("男")
{
public void info(){
System.out.println("代表男性");
}
},
FEMALE("女")
{
public void info(){
System.out.println("代表女性");
}
};
...
}

这样相当于定义了两个Gender的匿名子类实例,所有实际并没有创建Gender枚举类的实例。

这相等于这个枚举类是一个抽象类因为它并没有实现它实现的接口的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public enum Operation
{
PLUS
{
public double eval(double x, double y)
{
return x + y;
}
},
MINUS
{
public double eval(double x, double y)
{
return x - y;
}
},
TIMES
{
public double eval(double x, double y)
{
return x * y;
}
},
DIVIDE
{
public double eval(double x, double y)
{
return x / y;
}
};
// 为枚举类定义一个抽象方法
// 这个抽象方法由不同的枚举值提供不同的实现
public abstract double eval(double x, double y);
public static void main(String[] args)
{
System.out.println(Operation.PLUS.eval(3, 4));
System.out.println(Operation.MINUS.eval(5, 4));
System.out.println(Operation.TIMES.eval(5, 4));
System.out.println(Operation.DIVIDE.eval(5, 4));
}
}

上面例子四个枚举值PLUS,MINUS,TIMES,DIVIDE代表四种运算,该枚举类定义一个eval()方法进行运算。

我们可以让Operation枚举类定义一个eval()抽象方法,然后让四个枚举值去实现它,编译上面程序会产生五个class文件,四个匿名内部子类分别对应一个class文件,Operatin类对应一个class文件。

枚举类定义抽象方法时,不需要加上abstract,系统会自动加上,当定义每个枚举值时,必须实现这个抽象方法,否则会出现编译错误。


面向对象(下)
https://zhaoquaner.github.io/2022/05/11/Java/面向对象/面向对象(下)/
更新于
2022年5月22日
许可协议