Day12-内部类&代码块&枚举
学习目标
- 能够定义使用内部类
- 能够说出四种不同内部类的特点
- 能够定义并使用静态代码块
- 能够说出类和实例对象的初始化过程
- 能够说出代码块的调用顺序
- 能够定义枚举并说出枚举常见方法的用法
1. 内部类
在一个类A里,我们可以定义变量,也可以定义方法。思考一下,在A类里还能在定义一个B类吗?这种语法在Java里是允许的,这种情况下,B类可以被称为内部类,A类被称为外部类。
当一个类的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,不在其他地方单独使用,那么整个内部的完整结构最好使用内部类。
根据内部类定义的方式不同,可以讲内部类分为四种:
- 成员内部类
- 局部内部类
- 静态内部类
- 匿名内部类
注意:对于普通的类来说,我们只能使用缺省或者public权限修饰符;而对于有些内部类,可以使用全部的四种权限修饰符。
1.1 成员内部类
语法格式:
[修饰符] class 外部类{
[修饰符] class 内部类{
}
}
示例:
public class Test {
public static void main(String[] args) {
// 直接使用 new Outer().new Inner() 方法创建一个Inner对象
Outer.Inner inner1 = new Outer().new Inner();
// 创建一个 Outer 对象,再调用 Outer 对象的 getInner方法,在getInner方法里创建并返回 Inner 对象
Outer.Inner inner2 = new Outer().getInner();
}
}
class Outer {
int age = 19;
private String name = "jack";
private void test() {
System.out.println("我是Outer类里的test方法");
}
class Inner { // 可以把 Inner 想象成为和 age / name一样,只不过它的数据类型是class
// static int a = 10; 报错,不能使用 static
static final int b = 1; // 可以使用 static final 定义常量
private void test() {
System.out.println("我是Inner类里的test方法");
}
public void demo() {
// 内部类可以直接访问外部类的成员,包括私有成员。
System.out.println(name);
this.test(); // 等价于 test() 调用的是内部类的test方法
Outer.this.test(); // 需要使用 Outer.this 调用外部类的 test 方法
}
}
public static void foo() {
// Inner inner = new Inner(); 报错!外部static成员不允许访问非静态内部类
}
public Inner getInner() {
return new Inner();
}
}
成员内部类特点:
- 不能使用
static
关键字,但是可以使用static final
关键字定义常量。 - 内部类可以直接访问外部类的成员(包括私有成员)。
- 内部类如果想要调用外部类的方法,需要使用
外部类名.this.外部方法名
- 可以使用
final
修饰内部类,表示不能被继承。 - 编译以后也会有自己独立的字节码文件,只不过文件名是
Outer$Inner.class
- 外部函数的
static
成员里,不允许访问成员内部类。 - 可以在外部类里创建内部类对象然后再通过方法返回,同时,也可以使用
new Outer().new Inner()
在外部直接访问内部类。
1.2 局部内部类
局部内部类的使用和成员内部类的使用基本一致,只是局部内部类定义在外部类的方法中,就像局部变量一样,并不是外部类的成员。局部内部类在方法外是无法访问到的,但它的实例可以从方法中返回,并且实例在不再被引用之前会一直存在。
语法格式:
[修饰符] class 外部类{
[修饰符] 返回值类型 方法名([形参列表]){
[final/abstract] class 内部类{
}
}
}
示例:
class Outer {
int age = 19;
static int m = 10;
public void test() {
// final String y = "good";
String y = "good"; // JDK8 以后,final可以省略
class Inner { // 定义在外部类的某个方法里
// static int a = 10; 不能定义静态变量!
private void demo() {
System.out.println(age);
// 不能修改外部函数的局部变量
// y = 'yes';
// 只能访问被 final 修饰的外部函数的局部变量
// JDK8 以后,如果外部函数的局部变量没有加 final,编译器会自动加 final
System.out.println(y);
}
}
Inner inner = new Inner();
inner.demo();
}
}
局部内部类特点:
- 定义在类的某个方法里,而不是直接定义在类里。
- 局部内部类前面不能有权限修饰符!
- 局部内部类里不能使用static声明变量!
- 局部内部类中能访问外部类的静态成员。
- 如果这个局部内部类所在的方法是静态方法,它无法访问访问外部类的非静态成员。
- 局部内部类可以访问外部函数的局部变量,但是这个局部变量必须要被final修饰。JDK8以后,如果局部变量被局部内部类使用了,会自动在前面加final.
思考: 为什么局部变量前要加final.
public class TestInner{
public static void main(String[] args) {
A obj = Outer.method();
//因为如果c不是final的,那么method方法执行完,method的栈空间就释放了,那么c也就消失了
obj.a();//这里打印c就没有中可取了,所以把c声明为常量,存储在方法区中
}
}
interface A{
void a();
}
class Outer{
public static A method(){
final int c = 3;
class Sub implements A{
@Override
public void a() {
System.out.println("method.c = " + c);
}
}
return new Sub();
}
}
1.3 匿名内部类
语法格式:
new 父类名或者接口名(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
实例:
abstract class Animal {
abstract void shout();
}
class Demo {
public void demo() {
Animal animal = new Animal(){
@Override
void shout() {
System.out.println("动物正在叫");
}
};
}
}
匿名内部类的特点:
- 匿名内部类就是一种特殊的局部内部类,只不过没有名称而已,它的基本特点和局部内部类一致。
- 匿名内部类不能有构造器,匿名内部类没有类名,肯定无法声明构造器。
- 匿名内部类的前提是,这个内部类必须要继承自一个父类或者父接口!
- 匿名内部类是接口的一种常见简化写法,也是我们开发中最常使用的一种内部类。它的本质是一个
实现了父类或者父接口具体方法
的一个匿名对象。
1.4 静态内部类
静态内部类也被称为嵌套类,不同于前三种内部类,静态内部类不会持有外部类对象的引用。
语法格式:
[修饰符] class 外部类{
[其他修饰符] static class 内部类{ }
}
示例:
class Outer {
int age = 19;
static String type = "outer";
static class Inner { // 需要使用 static 关键字
static int x = 1; // 能够定义静态变量
public static void test() {
// 只能访问外部的静态变量,不能访问非静态变量
// System.out.println(age);
System.out.println(type);
}
}
public void demo() {
System.out.println(Inner.x); // 外部类可以直接通过 内部类.静态变量名,不需要创建对象
}
}
静态内部类特点:
- 使用
static
关键字修饰。 - 在静态内部类里,可以使用
static
关键字定义静态成员。 - 只能访问外部的静态成员,不能访问外部的非静态成员。
- 外部类可以直接通过
静态内部类名.静态成员名
访问静态内部类的静态成员。
其实严格的讲(在James Gosling等人编著的《The Java Language Specification》)静态内部类不是内部类,而是类似于C++的嵌套类的概念,外部类仅仅是静态内部类的一种命名空间的限定名形式而已。所以接口中的内部类通常都不叫内部类,因为接口中的内部成员都是隐式是静态的。例如:Map.Entry。
2. 代码块
Java中包括四种代码块:
- 普通代码块:就是类里方法体代码。
- 构造代码块:类里直接使用 {} 编写的代码。
- 静态代码块:在构造代码块前添加
static
关键字。 - 同步代码块:使用
synchronize
关键字包裹起来的代码块,用于多线程。
普通代码块就是方法体的代码,这里就不再赘述。下面我们来看一下构造代码块和静态代码块。
2.1 静态代码块
语法结构:
[修饰符] class 类名{
static{
静态代码块语句;
}
}
示例:
public class Demo {
public static void main(String[] args) {
System.out.println("我是Main方法");
Demo d = new Demo();
}
Demo() {
System.out.println("我是构造方法");
}
private static int a;
static {
a = 10; // 静态代码块可以用于对静态变量初始化
System.out.println("我是静态代码块");
}
}
特点:
- 使用
static
关键字修饰,写在类里,方法外,用来对类进行初始化。 - 一个类里可以有多个静态代码块,但是通常情况下只会定义一个。
- 随着类加载而执行,只执行一次,只要类加载就会执行,所以执行的优先级很高。
- 一般情况下,如果有些代码必须在项目启动的时候就执行的时候,就需要使用静态代码块。
2.2 类的初始化
类被加载内存后,会在方法区创建一个Class对象(后面反射章节详细学习)来存储该类的所有信息。此时会为类的静态变量分配内存,然后为类变量进行初始化。在加载类的过程中,JVM会调用<clinit>方法,专门来对类变量或者静态代码块进行初始化。
public class Test{
public static void main(String[] args){
Father.test();
}
}
class Father{
private static int a = getNumber();
static{
System.out.println("Father(1)");
}
private static int b = getNumber();
static{
System.out.println("Father(2)");
}
public static int getNumber(){
System.out.println("getNumber()");
return 1;
}
public static void test(){
System.out.println("Father:test()");
}
}
运行结果:
getNumber()
Father(1)
getNumber()
Father(2)
Father:test()
public class Test{
public static void main(String[] args){
Son.test();
System.out.println("-----------------------------");
Son.test();
}
}
class Father{
private static int a = getNumber();
static{
System.out.println("Father(1)");
}
private static int b = getNumber();
static{
System.out.println("Father(2)");
}
public static int getNumber(){
System.out.println("Father:getNumber()");
return 1;
}
}
class Son extends Father{
private static int a = getNumber();
static{
System.out.println("Son(1)");
}
private static int b = getNumber();
static{
System.out.println("Son(2)");
}
public static int getNumber(){
System.out.println("Son:getNumber()");
return 1;
}
public static void test(){
System.out.println("Son:test()");
}
}
// 运行结果
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Son:getNumber()
Son(1)
Son:getNumber()
Son(2)
Son:test()
-----------------------------
Son:test()
2.3 构造代码块
语法结构:
{
//代码块里的内容
}
示例:
package com.atguigu.java;
public class Demo {
public static void main(String[] args) {
System.out.println("我是main方法");
Demo d1 = new Demo();
System.out.println(d1.getName());
Demo d2 = new Demo();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
public Demo() {
System.out.println("我是构造方法");
}
{
System.out.println("我是构造代码块");
this.name = "jack";
}
}
// 我是main方法
// 我是构造代码块
// 我是构造方法
// jack
// 我是构造代码块
// 我是构造方法
特点:
- 对象一建立就会执行构造代码块,构造代码块先于构造方法执行,用来给对象进行初始化。
- 每创建一个对象都会调用一次构造代码块。
- 构造代码块是给所有对象统一初始化,构造函数是给各自对应的对象初始化。
- 如果每个实例对象都需要统一的初始化,可以考虑将这部分代码写入到构造代码块里。
2.4 实例对象的初始化
除了<clinit>方法初始化类以外,在创建实例对象时,还会自动调用<init>方法对实例对象进行初始化。
使用一个类来实例化对象的几种方式:
- 最常见的方式,使用关键字
new
来创建一个实例对象。 - 使用反射,调用
Class
或者java.lang.reflect.Constructor
对象的newInstance()
方法。 - 调用对象的clone()方法。
- 调用
java.io.ObjectInputStream
类的getOjbect()
方法序列化。
public class Test{
public static void main(String[] args){
Father f1 = new Father();
Father f2 = new Father();
}
}
class Father{
private int a = getNumber();
private String info;
{
System.out.println("Father(1)");
}
Father(){
System.out.println("Father()无参构造");
}
Father(String info){
this.info = info;
System.out.println("Father(info)有参构造");
}
private int b = getNumber();
{
System.out.println("Father(2)");
}
public int getNumber(){
System.out.println("Father:getNumber()");
return 1;
}
}
运行结果:
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Father()无参构造
Father:getNumber()
Father(1)
Father:getNumber()
Father(2)
Father(info)有参构造
2.5 代码块的执行顺序
先初始化类,加载类变量(static类型变量和static代码块),再调用构造代码块,然后再调用构造方法。
class Father {
Father() {
System.out.println("我是Father里的构造方法");
}
static {
System.out.println("我是Father里的静态代码块");
}
{
System.out.println("我是Father里的构造代码块");
}
}
public class Demo extends Father {
public static void main(String[] args) {
System.out.println("我是Main方法");
Demo d = new Demo();
}
Demo() {
System.out.println("我是Demo构造方法");
}
static {
System.out.println("我是Demo静态代码块");
}
{
System.out.println("我是Demo构造代码块");
}
}
// 我是Father里的静态代码块
// 我是Demo静态代码块
// 我是Main方法
// 我是Father里的构造代码块
// 我是Father里的构造方法
// 我是Demo构造代码块
// 我是Demo构造方法
3. 单例设计模式
单例模式,是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中应用该模式的类一个类只有一个实例,即一个类只有一个对象实例。例如,windows操作系统里的回收站。
步骤:
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static final类型。
- 定义一个静态方法返回这个唯一对象。
单例设计模式分为饿汉式(立即加载型)和懒汉式(延迟加载型)。由于懒汉式有线程安全问题,又衍生出了线程安全版的懒汉式以及DLC双检查锁(最佳实现方式)的懒汉式。
3.1 饿汉式
立即加载就是使用类的时候已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故又被称为“饿汉模式”),常见的实现办法就是直接new实例化。
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static final Singleton instance = new Singleton();
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
return instance;
}
}
- 优点:实现起来简单,没有多线程同步问题。
- 缺点:当类SingletonTest被加载的时候,会初始化static的instance,静态变量被创建并分配内存空间,从这以后,这个static的instance对象便一直占着这段内存(即便你还没有用到这个实例),当类被卸载时,静态变量被摧毁,并释放所占有的内存,因此在某些特定条件下会耗费内存。
3.2 懒汉式
延迟加载就是调用get()方法时实例才被创建(先不急着实例化出对象,等要用的时候才给你创建出来。不着急,故又称为“懒汉模式”),常见的实现方法就是在get方法中进行new实例化。
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 优点:实现起来比较简单,当类SingletonTest被加载的时候,静态变量static的instance未被创建并分配内存空间,当getInstance方法第一次被调用时,初始化instance变量,并分配内存,因此在某些特定条件下会节约了内存。
- 缺点:在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态。
4. 枚举类
有些时候,如果一个类的对象是有限并且固定的,可以考虑使用枚举类。在JDK1.5之前,需要自己手动的实现。
public class Test {
public static void main(String[] args) {
System.out.println(Season.SPRING);
}
}
class Season {
private Season() {
}
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();
}
4.1 Enum的使用
枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据。有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
语法格式:
enum 枚举类类名 {
对象1,对象2,对象3;
}
示例:
public enum Season{
// SPRING(),SUMMER(),AUTUMN(),WINTER(); 调用构造函数,创建了四个对象,小括号可以省略
SPRINT,SUMMER,AUTUMN,WINTER; // 创建了四个对象
// private Season(){} 构造方法可以不写,默认就有一个空参数构造方法
}
public enum WeekDay {
// 创建的实例对象时,必须要调用构造方法传入 name 参数
MON("周一"), TUE("周二"), WED("周三"), THU("周四"), FRI("周五"), SAT("周六"), SUN("周日");
private String name;
WeekDay(String name) { // 还能自定义构造方法
this.name = name;
}
// 像正常的类一样,可以使用 getter/setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
枚举特点:
- 所有的枚举都继承自java.lang.Enum类,由于Java 不支持多继承,所以枚举对象不能再继承其他类(但是可以实现接口)。
- 枚举类的所有实例对象都必须放在第一行展示,并且默认都是以
public static final
修饰的常量,所以变量名通常都全大写。 - 在创建实例对象时,不需使用new 关键字,也不需使用小括号显式调用构造器
- 使用enum定义、非抽象的枚举类默认使用final修饰,不可以被继承。
- 枚举类的构造器只能是私有的,不允许在外部创建对象。
4.2 常见方法
方法名 | 作用 |
---|---|
toString | 返回的是常量名(对象名),可以重写 |
name | 返回的是常量名(对象名),不推荐使用,建议使用toString |
values | 返回该枚举类的所有的常量对象,返回类型是当前枚举的数组类型。 |
valueOf | 根据常量名,返回一个枚举对象。 |
ordinal | 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。 |