Java类的嵌套
之前我们所知道父子类的继承类,接下来先描述内部类与静态嵌套类二者的区别(是否被static修饰),接着就是static关键字的用法,然后由静态属性到普通常量,再从普通常量到枚举常量,进而阐述枚举类型的用法。
1. 内部类与静态嵌套类
1.1 内部类(非静态嵌套类)
通常情况下,一个Java代码文件只定义一个类,即使两个类是父类与子类的关系,我们都要把其拆开成两个代码文件分别定义。但是某些事物相互之间有密切关系,又不同于父子类的继承关系,像树与花之间的依附关系,定义在一个类的成员类,俗称为“内部类”。
【代码】
//演示内部类的简单定义。这是一个树木类
public class Tree {
private String tree_name;
public Tree(String tree_name) {
this.tree_name = tree_name;
}
public void sprout() {
System.out.println(tree_name + "发芽啦");
// 外部类访问它的内部类,就像访问其它类一样,都要先创建类的实例,再访问它的成员
Flower flower = new Flower("花朵");
flower.bloom();
}
// Flower类位于Tree类的内部,它是个内部类
public class Flower{
private String flower_name;
public Flower(String flower_name) {
this.flower_name = flower_name;
}
public void bloom() {
System.out.println(flower_name + "开花啦");
}
}
}
内部类访问的注意点:
- 外部类访问内部类:先创建内部类的实例,再访问它的成员。(代码如上)
- 其他地方访问内部类:先创建外部类的实例,再基于该实例创建内部类实例。
内部类实例的创建代码格式 “ 外部类的实例名.new 内部类的名称(…) ”
【代码】
// 先创建外部类的实例,再基于该实例去创建内部类的实例
Tree tree = new Tree("杨树"); // 创建一个树木实例
tree.sprout(); // 调用树木实例的sprout方法
Tree.Flower flower = tree.new Flower("杨花"); // 通过树木实例创建内部类的花朵实例
flower.bloom(); // 调用花朵实例的bloom方法
- 内部类访问外部类:必须在this之前添加“外部类的名称”,即“ 外部类的名称.this ”
// 该方法访问外部类TreeInner的tree_name字段
public void bloomOuterTree() {
// 要想在内部类里面访问外部类的成员,就必须在this之前添加“外部类的名称.”
System.out.println(Tree.this.tree_name + "的" + flower_name
+ "开花啦");
}
1.2 静态嵌套类
在内部类的定义代码前面添加关键字static,表示这是一种静态的内部类,称为“静态嵌套类”,它无须强制绑定外部类的实例即可正常使用。
【代码】
public class TreeNest {
//演示静态嵌套类的定义
private String tree_name; // 树木名称
public TreeNest(String tree_name) {
this.tree_name = tree_name;
}
public void sprout() {
System.out.println(tree_name + "发芽啦");
}
// Flower类虽然位于TreeNest类的里面,但是它被static修饰,故而与TreeNest类的关系比起一般的内部类要弱。
// 为了与一般的内部类区别开来,这里的Flower类被叫做静态嵌套类。
public static class Flower {
private String flower_name; // 花朵名称
public Flower(String flower_name) {
this.flower_name = flower_name;
}
public void bloom() {
System.out.println(flower_name + "开花啦");
}
public void bloomOuterTree() {
// 注意下面的写法是错误的,静态嵌套类不能直接访问外层类的成员
// System.out.println(TreeNest.this.tree_name+"的"+flower_name+"开花啦");
TreeNest treeNest = new TreeNest("桃树");
System.out.println(treeNest.tree_name + "的" + flower_name + "开花啦");
}
}
}
静态嵌套类访问的注意点:
- 静态嵌套类访问外层类:先创建对方的实例,然后才能通过实例访问它的每个成员。(代码如上)【区别:与外部类访问内部类一样】
- 其他地方访问静态嵌套类:无须动态创建外层类的实例,直接创建静态嵌套类的实例就行。
静态嵌套类实例的创建“ new 外层类的名称.静态嵌套类的名称(…) ”
【代码】
private static void testNest() {
// 创建一个静态嵌套类的实例,格式为“new 外层类的名称.静态嵌套类的名称(...)”
TreeNest.Flower flower = new TreeNest.Flower("茉莉花");
flower.bloom(); // 调用静态嵌套类实例的bloom方法
}
2. 静态:关键字static的用法
static关键字可以用来修饰代码块表示静态代码块,修饰成员变量表示全局静态成员变量,修饰方法表示静态方法。(注意:不能修饰普通类,除了内部类,这是为什么?)
class A {
static {
System.out.println("A : 静态代码块");
}
static int i ; // 静态变量
static void method() {
System.out.println("A: 静态方法");
}
}
简而言之,被static关键字修饰的内容都是静态的。
静态是相对于动态的。
动态是指Java程序在JVM上运行时,JVM会根据程序的需要动态创建对象并存储对象(分配内存) ,对象使命结束后,对象会被垃圾回收器销毁,即内存回收由JVM统一管理并分配给其他新创建的对象;
静态是指Java程序还没有运行时,JVM就会为加载的类分配空间存储被static关键字修饰的内容 ;如静态成员变量,Java类加载到JVM中,JVM会把类以及类的静态成员变量存储在方法区,我们知道方法区是线程共享且很少发生GC的区域,所以被static关键字修饰的内容都是全局共享的,且只会为其分配一次存储空间。
所以当类的某些内容不属于对象,而由对象共享即属于类的时候,就可以考虑是否用static关键字进行修饰。
static关键字的作用
A. 修饰代码块
类中用static关键字修饰的代码块称为静态代码,反之没有用static关键字修饰的代码块称为实例代码块。
实例代码块会随着对象的创建而执行,即每个对象都会有自己的实例代码块,表现出来就是实例代码块的运行结果会影响当前对象的内容,并随着对象的销毁而消失(内存回收);而静态代码块是当Java类加载到JVM内存中而执行的代码块,由于类的加载在JVM运行期间只会发生一次,所以静态代码块也只会执行一次。
因为静态代码块的主要作用是用来进行一些复杂的初始化工作,所以静态代码块跟随类存储在方法区的表现形式是静态代码块执行的结果存储在方法区,即初始化量存储在方法区并被线程共享。
static {
//这里是被static修饰的代码段内容
}
B.修饰成员变量
类中用static关键字修饰的成员变量称为静态成员变量,因为static不能修饰局部变量(为什么?),因此静态成员变量也能称为静态变量。
静态变量跟代码块类似,在类加载到JVM内存中,JVM会把静态变量放入方法区并分配内存,也由线程共享。访问形式是:类名.静态成员名。
public class StaticTest {
public static void main(String[] args) {
System.out.println(D.i);
System.out.println(new D().i);
}
}
class D {
static {
i = 2;
System.out.println("D : 静态代码块1");
}
static int i;
}
运行结果
D : 静态代码块1
2
2
静态变量存储在类的信息中,且可以在线程间共享,那么它当然也属于该类的每个对象,因此可以通过对象访问静态变量,但编译器并不支持这么做,且会给出警告。
注意:
- 一个类的静态变量和该类的静态代码块的加载顺序。类会优先加载静态变量,然后加载静态代码块,但有多个静态变量和多个代码块时,会按照编写的顺序进行加载。
class D {
static {
i = 2 ;
System.out.println("D:静态代码块1");
}
static {
i = 6;
System.out.println("D:静态代码块2");
}
static int i ;
}
- 静态变量可以不用显式的初始化,JVM会默认给其相应的默认值。如基本数据类型的byte为0,short为0,char为\u0000,int为0,long为0L,float为0.0f,double为0.0d,boolean为false,引用类型统一为null。
- 静态变量既然是JVM内存中共享的且可以改变,那么对它的访问会引起线程安全问题(线程A改写的同时,线程B获取它的值,那么获取的是修改前的值还是修改后的值呢?),所以使用静态变量的同时要考虑多线程情况。如果能确保静态变量不可变,那么可以用final关键字一起使用避免线程安全问题;否则需要采用同步的方式避免线程安全问题,如与volatile关键字一起使用等。
- static关键不能修饰局部变量,包括实例方法和静态方法,不然就会与static关键字的初衷-共享相违背。
C.修饰方法
用static关键字修饰的方法称为静态方法,否则称为实例方法。
通过类名.方法名调用,但需要注意静态方法可以直接调用类的静态变量和其他静态方法,不能直接调用成员变量和实例方法(除非通过new一个对象调用)。
class D {
static {
i = 2;
System.out.println("D : 静态代码块");
}
static final int i;
int j;
static void method() {
System.out.println(i);
System.out.println(new D().j);
method1();
new D().method2();
}
static void method1() {
System.out.println(i);
}
void method2() {
System.out.println(i);
}
}
注意: 既然类的实例方法需要对象调用才能访问,而静态方法直接通过类名就能访问,那么在不考虑部署服务器的情况下,一个类是如何开始执行的呢?最大的可能就是通过“类名.静态方法”启动Java,而我定义那么多静态方法,JVM又是如何知道主入口呢?
或许,你想到了main方法。
没错,就是main方法被Java规范定义成Java类的主入口。Java类的运行都由main方法开启:
public static void main(String[] args) {
for (String arg : args) {
// 参数由外部定义
System.out.println(arg);
}
}
但注意main并不是Java关键字,它只是一个规定的程序入口的方法名字;另外main方法可以重载。
注意: static关键字虽然不能修饰普通类,但可以用static关键字修饰内部类使其变成静态内部类。 static关键字本身的含义就是共享,而Java类加载到JVM内存的方法区,也是线程共享的,所以没必要用static关键字修饰普通类。
static关键字的缺点
封装是Java类的三大特性之一,也是面向对象的主要特性。因为不需要通过对象,而直接通过类就能访问类的属性和方法,这有点破坏类的封装性;所以除了Utils类,代码中应该尽量少用static关键字修饰变量和方法。
3. 枚举类型
之前我们所知道的定义一个常量是用public static final***,这种定义方式只适用于表达一种信息,对于复杂、安全性高的常量,则力不从心了。
所以,接下来了解枚举,所谓枚举就是指某些同类型常量的有限集合。使用enum来标识,一个简单的枚举定义只需要一个名称列表。
//演示枚举类型的简单定义。这是个季节枚举
public enum Season {
// 几个枚举项(变量)之间以逗号分隔
SPRING, SUMMER, AUTUMN, WINTER
}
外部访问枚举类型的格式:枚举类型的名称.枚举项的名称
由于ordinal方法和toString方法是枚举类型enum自带的保留方法,但是这两个方法是系统自带的,无法满足开发者的需求。
又因为枚举类型enum本来就来源类class,故而可以把枚举当作类一样定义,也就是说,枚举类允许定义自己的成员属性、方法以及构造方法。
因此,定义相应的方法可以满足开发者需求,所有技术都同以往一样。
【代码:枚举定义】
//演示枚举类型的扩展定义
public enum SeasonCn {
// 在定义枚举变量的同时,调用该枚举变量的构造方法
SPRING(1, "春天"), SUMMER(2, "夏天"), AUTUMN(3, "秋天"), WINTER(4, "冬天");
private int value; // 季节的数字序号
private String name; // 季节的中文名称
// 在构造方法中传入该季节的阿拉伯数字和中文名称
private SeasonCn(int value, String name) {
this.value = value;
this.name = name;
}
// 获取季节的数字序号
public int getValue() {
return this.value;
}
// 获取季节的中文名称
public String getName() {
return this.name;
}
}
注意: 在枚举项列表中把每个枚举项都换成携带构造方法的枚举声明,表示该枚举项是由指定构造方法生成的。
【代码:枚举调用】
// 演示扩展枚举类型的调用方式
private static void testEnumCn() {
SeasonCn spring = SeasonCn.SPRING; // 声明一个春天的季节实例
SeasonCn summer = SeasonCn.SUMMER; // 声明一个夏天的季节实例
SeasonCn autumn = SeasonCn.AUTUMN; // 声明一个秋天的季节实例
SeasonCn winter = SeasonCn.WINTER; // 声明一个冬天的季节实例
// 通过扩展而来的getName方法,可获得该枚举预先设定的中文名称
System.out.println("spring 序号=" + spring.getValue() + ", 名称=" + spring.getName());
System.out.println("summer 序号=" + summer.getValue() + ", 名称=" + summer.getName());
System.out.println("autumn 序号=" + autumn.getValue() + ", 名称=" + autumn.getName());
System.out.println("winter 序号=" + winter.getValue() + ", 名称=" + winter.getName());
}
枚举常见用法
A. 常量
我们定义常量都是: public static final… 。现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
public enum Color {
RED, GREEN, BLANK, YELLOW
}
B.switch
switch语句只支持byte,short,char,int,枚举,String(不能为浮点型和long型,boolean),使用枚举,能让我们的代码可读性更强。
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
C.覆盖枚举的保留方法
代码如上所讲
D.实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
public interface Behaviour {
void print();
String getInfo();
}
public enum Color implements Behaviour{
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//接口方法
@Override
public String getInfo() {
return this.name;
}
//接口方法
@Override
public void print() {
System.out.println(this.index+":"+this.name);
}
}
…