C语言是一种面向过程的语言,在现代软件开发过程中面临一些难以解决的问题,比如程序规模大到一定程度后难以团队开发和维护等,就产生了软件危机。于是人们从C语言的结构体中得到了启示,将其逐渐扩展到类,最终形成了面向对象的,新的编程方法。但是面向过程的语言是基础,面向对象中的方法还是面向过程的形式。
面向对象的是核心是,以类来组织代码,以对象来组织数据。
Java语言是一种非常受欢迎的面向对象的开发语言。类可以看做一种自定义的数据类型。从概念上来讲,类是某一类事物的抽象,所谓抽象,就是指抽出象的部分,即提取共同点。对象则是类的具体存在。程序使用类的构造器来创建对象。
面向对象除了类和对象的概念外,还有三大特征:封装、继承和多态。Java提供了private、protected和public三个访问控制符来实现封装,提供了extends关键字来让子类继承父类,
1、类和对象
类和对象是面向对象设计语言中的两个重要概念。
除了8种基本类型之外,类可以当做一种自定义数据类型,可以使用类来定义变量,这种类型的变量称为引用变量。
private String name = “Tom”;
private School s = new School();
1.1 定义类
[public|protected|private][abstract][final] class 类名{
[public|protected|private][static][final] 零到多个成员变量
[public|protected|private][static] 零到多个初始化块
[public|protected|private]零到多个构造方法
[public|protected|private][static][final][abstract] 零到多个普通方法
[public|protected|private][static] 零到多个内部类
}
修饰符可以是public、final、abstract或者完全省略,static不能修饰类,可以修饰内部类
类名的首字母要大写
static修饰的成员不能调用非static修饰的成员
成员变量用于定义该类或者类的实例所包含的状态数据,方法定义了该类的功能,构造器用于构造该类的实例,构造器是类创建对象的根本途径,若一个类没有构造器则无法创建实例,所以Java默认为类提供了一个无参构造,而一旦程序员为一个类提供了构造器,默认构造器即失效。
Java修饰符适用范围总表
修饰符 | 外部类/接口 | 成员属性 | 方法 | 构造器 | 初始化块 | 成员内部类 | 局部成员 |
---|---|---|---|---|---|---|---|
public | * | * | * | * | * | ||
protected | * | * | * | * | |||
包访问权限 | * | * | * | * | * | ||
private | * | * | * | * | |||
abstract | * | * | * | ||||
final | * | * | * | * | * | ||
static | * | * | * | * | |||
strictfp | * | * | * | ||||
synchronized | * | ||||||
native | * | ||||||
transient | * | ||||||
volatile | * |
- 初始化块和局部成员不能使用任何访问控制符,所以看起来像是使用了包访问控制符。
- strictfp表示EP-strict,也就是精确浮点的意思。默认Java浮点运算不够精确,若想更精确,就可以使用strictfp关键字来修饰类、接口和方法。
- native关键字主要用于修饰一个方法,被修饰的方法类似于一个抽象方法,通常使用C语言来实现。但是一旦使用native方法,这个程序将失去夸平台功能。
成员变量的定义格式:
[修饰符] 类型 成员变量名 [=默认值];
修饰符:可以省略,也可以是public、protected、private、static、final,其中前三个互斥,只能出现一个,但可以与static、final组合起来修饰成员变量。
如定义常量常使用:
public static final double PI = 3.1415926;
类型:类型可以使用8种基本类型和引用类型。
方法的定义格式:
[修饰符] 方法返回值类型 方法名(形参列表) {
// 零到多条可执行语句组成的方法体
}
修饰符:可以省略,也可以是public、protected、private、static、final、abstract,其中前三个互斥,只能出现一个,final和abstract互斥、但他们可以与static组合起来修饰方法。
返回值类型:可以是8种基本类型或者引用类型,若没有返回值则必须使用void来声明没有返回值。
static是个特殊的关键字,可以用来修饰成员变量、成员方法、内部类和初始化块,表示它属于这个类本身,而不是属于该类的单个实例。只是表示这种归属的。
构造器是个特殊的方法,它的定义格式:
[修饰符] 构造器名(形参列表){
//零到多条可执行性语句组成的构造器执行体
}
修饰符:可以省略,也可以是public、protected、private其中之一,不支持其他三种
构造器名: 必须和类名相同
构造器的返回值类型总是当前类,无须定义返回值类型,也不能使用return语句,如果定义了,编译器不会报错,只会把这个所谓的构造器当做普通方法来处理。
1.2、对象的创建
Student s1 = new Student();
s1.name = "Tom";
s1.age = 18;
s1.study("English");
School mySchool = new School();
s1.school = mySchool;
Student s2 = new Student();
// ...
内存分析:
- 当执行第一个单词Student时,JVM虚拟机会在方法区中查找是否有这个类,如果已经加载这个类的信息的话,则不再加载,直接使用;如果没有这个类的信息,则去classpath(类路径)下去查找字节码文件,加载到方法区中,若找不到,则报class not found异常,通过类加载器class loader加载类后,在方法区就有了student类的信息。类的信息包括:
- 代码、属性和方法
- static的变量
- 常量池,如类中出现的所有字符串常量
- s1是局部变量,存放在栈中,栈中局部变量,系统不会像类中成员变量那样帮你赋初值。
- new Student();创建对象放到堆中,创建对象时,成员变量会有默认值,六种数字类型都是0,小数是0.0,boolean默认为false,char默认为\u0000,2个字节,总之每位都是0,八种基本类型之外的字段都是null,成员方法会通过引用指向方法区中的代码。创建好对象后,在堆中是个连续的空间,会把首地址赋给栈中的s1局部变量。
- s.1name = “Tom”; s.name,是对象的成员变量,可以在堆中找到。”Tom”是字符串,也是非基本类型,所以可以通过引用来调用常量池中的值,若是基本数据类型,可以直接赋值。
- s1.study(“English”); 执行方法代码,执行方法时会在栈中开辟一块内存空间,方法执行完后,这个内存空间会被销毁。栈中存储相应的参数信息,并传递到代码的形参中。Java中所有的传递都是值传递。
1.3、对象的this引用
Java提供了this关键字,它总是指向调用该方法的对象。
根据this出现位置的不同,this作为对象的默认引用有两种情形:
1. 构造器中引用该构造器正在初始化的对象;
2. 在方法中引用调用该方法的对象。
this关键字的最大作用就是让类中的一个方法,访问该类里的另一个方法或者成员属性。
典型例子,dog类中的,run方法调用同类中的jump方法,使用this来调用。也可以省略这个this,即一个方法访问该类中定义的其他方法和成员变量时加不加this前缀的效果是一样的。但有种特殊情况,成员方法中的局部变量和类的成员变量重名时,若要调用类的成员变量,则必须使用this前缀。
使用static修饰的成员变量和方法是不能用this来调用的。
this可以作为方法的返回值,则可以多次连续调用同一个方法,从而使代码更加简洁,类似jQuery的。缺点是模糊了语义。
public class Tree{
public int age;
public Tree grow(){
age++;
return this;
}
}
Tree t = new Tree();
t.grow().grow().grow();
syso(t.age); //3
2、成员变量和局部变量
Java中根据变量位置的不同,可以将变量分为两大类,成员变量和局部变量。他们的运行机制存在较大的差异。
2.1 成员变量和局部变量的定义
成员变量分为,类成员变量和实例成员变量
类成员变量随着类的存在而存在,实例成员变量随着实例的存在而存在,所以类成员变量的作用域更大。
局部变量分为,形参、方法局部变量、代码块局部变量,共同点是作用域都仅限于方法的区域,方法结束即失效。
成员变量在类加载时会默认初始化,但是局部变量除形参外,都必须显示初始化。
2.2 成员变量的初始化和内存中的运行机制
当系统加载类或创建类的实例时,系统自动为成员变量分配内存空间,并指定初始值。
类初始化后,系统自动为类创建一个类对象。
Person p = new Person();
p.name = “Tom”;
存储变化的示意图
2.3 局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显示初始化才能使用,系统不会为局部变量执行初始化,即也不会为这个变量分配内存空间,直到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或对象,因此它总保存在其所在方法的栈内存中。
如果局部变量是8种基本类型之一,则直接将这个变量值保存到对应的内存中,如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
栈内存中的变量无需垃圾回收,随方法或代码块的运行结束而结束。
3.4 变量的使用规则
什么时候使用成员变量,什么时候使用局部变量呢?
当我们定义一个成员变量时,会将其放到堆内存中,成员变量的作用域将扩大到类或对象的存在范围。这有两个坏处:
- 增大了变量的生存时间,这将导致更大的内存开销;
- 扩大了变量的作用域,不利于提高程序的内聚性。
所以,我们应该有限制的使用成员变量,即当:
- 需要定义的变量是用于描述类或对象的信息的,例如,如果是人类的眼睛,每个人对象都是相同的2只眼睛,则应该定义为类的成员变量,如果是人的身高体重等个体性强的信息,则应该定义为对象成员变量。
- 如果需要以一个变量来保存该类或实例运行时的状态信息,例如定义棋盘数组用于其后保存棋盘的运行状态信息。
- 某个信息需要在类的多个方法之间进行共享的
- 使用局部变量时,也应该尽可能的缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中停留的时间就越短,程序的运行性能就越好,因此,能用代码块局部变量的地方,就坚决不要使用方法局部变量。
3、方法详解
方法是类或对象的行为特征的抽象,从功能上来看,方法完全类似于传统面向过程设计语言中的函数。
但是他们也有明显的区别:
在结构化编程语言中,函数是一等公民,整个软件由一个个函数组成,在面向对象编程语言中,类才是一等公民。Java中的方法不能独立存在,所有的方法都必须定义在类中。
3.1 方法的所属性
方法不是独立存在的实体,只能作为类和对象的附属,主要体现在:
- 方法不能独立定义,只能在类中定义
- 从逻辑意义上来看,方法要么属于类本身,要么属于类的一个对象
- 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。
3.2 方法的参数传递机制
Java方法的参数传递方式只有一种:值传递。
所谓值传递,就是将实际参数值的副本传入方法内,而参数本身不会受到任何影响。这个传入的参数,不管遇到什么更改都不会改变外部原有的情况,操作的都是这个副本。
例如:一个swap(int a, int b)
方法,将传入的两个值进行交换,则外部的a,b不会受到任何影响。
Java对于引用类型的参数传递,一样采用的是值传递方式。
不过这个传递的值是地址,地址是值传递,它指向的是对象。所以实际操作的是对象,看起来是引用传递,其实还是值传递。
进入一个方法,即进入了这个方法独有的方法栈。
3.3 形参个数可变的方法
从JDK1.5 开始,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
public class Varages{
public static void test(int a, String... books){
for(String tmp: books){
System.out.println(tmp);
}
}
public static void mian(String[] args){
test(5, "Java", "Spring", "Oracle");
}
}
形参个数可变的参数其实就是个数组参数,即
public static void test(int a, String... books);
public static void test(int a, String[] books);
区别是,调用更简洁,直接罗列,不用写成数组的形式,
test(5, "Java", "Spring", "Oracle");
如果写成数组的形式,则调用时必须
test(5, new String[]{"Java", "Spring", "Oracle"});
但是,数组形式的参数可以位于形参列表的任意位置,但是个数可变的形参只能处于形参列表的最后,也就是说,一个方法中最多只能有一个长度可变的形参。
3.4 递归方法
一个方法体内,调用它自身,称为方法递归。它会重复执行某段代码,这个重复执行无须循环控制。
递归有一条最重要的规定,递归一定要向已知方向递归。
递归是非常有用的,如果我们希望便利某个路径下的所有文件,但这个路径的深度是未知的,就可以使用递归来实现这个需求。
public static int f(int n){
if(n == 1 || n == 2)
return 1;
else
return f(n - 1) + f(n - 2);
}
3.5 方法重载
方法重载,即方法名相同,参数列表不同。方法的其他部分,如方法返回值类型、修饰符等,与方法重载无关。
Java中不能使用返回值类型作为区分方法重载的依据。因为Java调用时可以忽略返回值,即int f(){}和void f(){}两个方法,都可以使用f()调用,无法区分调用的是那个。
即,Java程序中唯一确定一个方法,需要同时满足三个要素
- 调用者,方法的所属者,可以是类,也可以是对象
- 方法名
- 形参列表
4、深入构造器
构造器是个特殊的方法,用于创建实例时执行初始化,即使使用工厂模式、反射等方式创建对象,其实质仍然依赖于构造器,因此Java类必须包含一个或以上的构造器。
普通的方法通过对象来调用,构造方法通过new来调用,因为对象都没有所以不能通过对象来调用。
- 方法名和类名必须完全一致
- new创建
- 没有返回类型,不能使用return
- 如果没有构造器,则编译器默认提供一个无参的,若有构造器则不提供
4.1 使用构造器执行初始化
构造器最大的用处就是在创建对象时执行初始化。
当创建一个对象时,系统为这个对象的成员变量进行默认初始化,这种默认初始化将所有基本类型的成员变量都设为0,把所有引用类型的成员变量都设置为null。
构造器是创建对象的重要途径,通过new关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。
实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了。这些操作在构造器执行之前就都完成了,只是这个对象还不能被外部程序访问,只能在构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常会赋值给另一个引用类型的变量,从而让外部程序可以访问该对象。
一旦程序员提供了自定义的构造器,系统就不再提供默认的构造器,如果用户希望保留无参数的构造器,可以提供多个构造器,这就是构造器的重载。
4.2 构造器的重载
同一个类中有多个构造器,多个构造器的形参列表不同,即称为构造器重载。
5、初始化块
与构造器作用非常类似的是初始化块,它可以对Java对象进行初始化操作。
5.1 使用初始化块
初始化块是Java类中可出现的第4种成员,一个类中可以有多个初始化块,相同类型的初始化块之间有顺序关系,先定义先执行。
初始化块只在创建Java对象时隐式执行,而且在执行构造器之前执行。
当Java创建一个对象时,系统先为该对象的所有实例成员变量分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,其初始化顺序是:先执行初始化块或声明成员变量时指定的初始值,再执行构造器里指定的初始值。
语法格式:
[修饰符] {
//块中可执行代码
}
修饰符只能是static,使用static修饰的称为静态初始化块。
private int a = 0;
public InterfaceTest(){
this.a = 1;
System.out.println("constructor");
}
{
a = 2;
System.out.println("init block");
}
public static void main(String[] args){
InterfaceTest it = new InterfaceTest();
System.out.println(it.a);
}
/*输出
init block
constructor
1*、
5.2 初始化块和构造器
初始化块是构造器的补充,初始化块总在构造器执行之前执行。与构造器不同的是,初始化块是一段固定执行的代码,它不能接收任何参数,因此对同一个类的所有对象执行的处理完全相同。所以,对于这种所有对象的公共处理,可以提取到初始化块中执行。
构造器中不同的对象可以传入不同参数。
与构造器类似的,创建一个Java对象时,不仅会执行该类的普通初始化块和构造器,也会一直上溯到java.lang.Object类,先执行Object类的初始化块…
5.3 静态初始化块
静态初始化块,也称为类初始化块。静态初始化块是类相关的,系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。
静态初始化块是类相关的,通常对类成员变量进行初始化处理,静态初始化块不能对实例成员变量进行初始化。
与普通初始化块类似的是,系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且还会一直上溯到java.lang.Object类的,会先执行Object类的静态初始化块…
6、内部类
大部分的时候,我们把类定义为一个独立的程序单元,在某些情况下,我们把一个类放在另一个类的内部定义,这个定义在其他类内部的类,就叫做内部类。
内部类的作用主要有:
- 内部类可以提供更好的封装,可以把内部类隐藏在外部类之中,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,因为内部类可以被当做外部类的成员,同一个类的成员间可以相互访问,但外部类不能访问内部类的实现细节。
- 匿名内部类使用创建那些仅需要一次使用的类。
- java中的内部类和接口加在一起,可以的解决常被C++程序员抱怨java中存在的一个问题 没有多继承。内部类可以实现接口。
6.1 非静态内部类
内部类的定义非常简单,只需要把一个类放在另一个类内部定义即可。此处的类内部,包括类中的任何位置,甚至在方法中也可以定义内部类(在方法中定义的内部类称为局部内部类)。
大部分时候,内部类都作为成员内部类来定义,局部内部类和匿名内部类不是类成员。
成员内部类分为静态内部类和非静态内部类。
普通类的上一级程序单元是包,所以它只有两个作用域:同一个包内和任何位置。因此,它只有两种访问权限,包访问权限和公开访问权限,对应省略访问控制符和public访问控制符。类没有protected访问修饰符。
内部类的上一级程序单元是外部类,它具有4个作用域,同一个类中,同一个包中,子类中和任何位置,对应private、默认、protected和public 4种访问控制权限。
public class Cow{
private double weight;
public Cow(double weight){
this.weight = weight;
}
private class CowLeg{
private double length;
public void getLength(){
syso("cow leg length = " + length + "; weight = " + weight); //直接调用外部类的成员变量
}
}
public void test(){
CowLeg cl = new CowLeg();
cl.getLength();
}
}
Cow cow = new Cow(345.67);
cow.test();
编译上面程序,可以在文件的路径下找到两个class文件,一个是Cow.class,另一个是Cow$CowLeg.class(内部类的class文件总是OuterClass$InnerClass.class)
非静态内部类中可以直接访问外部类的private实例变量,这是因为在非静态内部类对象中,保存了一个它寄存的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,而非静态内部类实例必须寄存在外部类实例中)
当非静态内部类的方法访问某个变量时,
- 系统优先在该方法中查找是否存在该名字的局部变量,如果存在就使用;如果不存在,
- 则到该方法所在的内部类中查找是否存在该名字的成员变量,有则使用;如果不存在,
- 则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果有则使用,如果没有则编译错误,提示找不到。
此外,如果外部类成员变量、内部类成员变量和内部类方法中的局部变量重名时,可通过使用this、外部类类名.this作为限定来区分。
非静态内部类的成员可以访问外部类的private成员,但是反过来则不可以。非静态内部类的成员只在非静态内部类范围内可知,不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显示创建非静态内部类对象来调用方法访问其实例成员。
若不显示创建内部类,则内部类不存在,无法访问。
非静态内部类中不允许定义静态成员。不能有静态成员变量、静态方法、静态初始化块。但是静态内部类既可以有静态成员,也可以有非静态成员。
public class Outer {
class Inner {
//public static int si = 1;错误,不允许存在静态的内部成员
public static final int i = 1;
}
}
原来,Java编译的原则是,如果类中有静态的东西,那么在加载该类的时候,就要为所有静态成员成员分配内存空间(应该是在方法区)。而对于非静态成员,则是在该类实例化的时候才分配内存空间。
对于java类加载顺序我们知道,首先加载类,执行static变量初始化,接下来执行对象的创建,如果我们要执行代码中的变量si初始化,那么必须先执行加载OuterClass,再加载Innerclass,最后初始化静态变量si,问题就出在加载Innerclass上面,我们可以把InnerClass看成OuterClass的非静态成员,它的初始化必须在外部类对象创建后以后进行,要加载InnerClass必须在实例化OuterClass之后完成,java虚拟机要求所有的静态变量必须在对象创建之前完成,这样便产生了矛盾。
为什么加了final就可以了呢,并且是通过Outer.Inner.i的方式访问,是不是违规了。其实这里Outer.Inner.i并不是通过变量来访问的,他在编译期已经确定了值,因为编译器会对static final 声明的变量做优化,会用常量替换static final声明的变量,因此,这里用常量1去代替了这里的Outer.Inner.i。非静态内部类的static final 只能用在基本类型(int,short,long byte,double,float, char,boolean)和字符串(String)类型上(数组也不行)。
6.2 静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。
静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
静态内部类不能访问外部类的实例成员,这是因为,静态内部类对象不是寄存在外部类对象里的,而是寄存在外部类的类本身中的。当静态内部类对象存在时,并不存在一个被它寄存的外部类对象,静态内部类对象里只有外部类的类引用,而没有外部类对象的引用。所以静态内部类的实例方法调用外部类的实例成员,会找不到,会报错。
静态内部类是外部类的一个静态成员,因此静态内部类可以访问外部类的静态成员。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,或者静态内部类对象作为调用者来访问静态内部类的实例成员。
class OutClass{
static class InnerClass{
private static int prop1 = 5;
private int prop2 = 6;
}
public void accessInnerClass(){
System.out.println(InnerClass.prop1); //5
System.out.println(new InnerClass().prop1); //5
//System.out.println(InnerClass().prop2); //编译不通过
System.out.println(new InnerClass().prop2); //6
}
}
Java还允许在接口中定义内部类。
6.3 使用内部类
定义类的主要作用就是定义变量、创建实例和做为父类被继承。
定义内部类的主要作用类似,但也有一些小的差异:
- 在外部类内部使用内部类
与使用普通类基本一样 在外部类以外使用非静态内部类
- 如果希望在外部类以外的地方访问内部类,则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。
- 省略访问控制符的内部类,只能被与外部类同一个包中的其他类所访问。
- 使用protected修饰的内部类,可被与外部类同一个包中的其他类和外部类的子类所访问。
- 使用public修饰的内部类,可以在任何地方被访问。
因为非静态内部类的对象必须寄存在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法为:
OuterInstance.new InnerConstructor()
class Out{ class In{ public In(String msg){ System.out.println(msg); } } } Out out = new Out(); Out.In in = out.new In("hello"); //非静态内部类的构造器必须使用外部类对象来调用
当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,但是调用非静态内部类的构造器时,必须存在一个外部类对象。
class Sub extends Out.In{ public Sub(Out out){ out.super("hello"); //内部类In类的构造器必须使用外部类对象来调用,代表调用In类的构造器 } }
在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建内部类对象时无需创建外部类对象。
在外部类以外的地方创建静态内部类实例的语法如下
new OuterClass.InnerConstructor()
class StaticOut{ static class StaticIn{ public StaticIn(String msg){ System.out.println(msg); } } } StaticOut.StaticIn oi = new StaticOut.StaticIn("static inner class constructor");
从上面的代码来看,不管是静态内部类还是非静态内部类,他们声明变量的语法是一样的,区别是创建内部类对象时,静态内部类只需使用外部类即可调用构造器,但是非静态内部类必须使用外部类对象来调用构造器。
因为调用静态内部类的构造器时无须使用外部类对象,所以创建静态内部类的子类也很简单
public class StaticSub extends StaticOut.StaticIn{}
当定义一个静态内部类时,其外部类非常像一个包空间。使用静态内部类比非静态内部类要简单很多,只要把外部类当做包空间即可。所以当程序需要室内内部类时,有限考虑使用静态内部类。
内部类是外部类的成员,那么可否为外部类定义子类,在子类中再定义一个内部类来重写父类中的内部类?
不可以,及时子类的内部类名字相同,也不是重写,因为他们所处的外部类不同,所以不可能完全重名,也就不能被重写。
6.4 局部内部类
如果把一个内部类放在方法中定义,那么这个内部类就是局部内部类。局部内部类仅在该方法中有效,不能在外部类以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
任何局部成员,它的上一级都是方法,而不是类,因此使用static修饰她没有意义。
局部成员的作用域是方法,其他程序永远也不能访问另一个方法中的局部成员,所以局部成员不需要使用访问控制符。
如果需要用局部内部类定义变量、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行。
public class Test{
public static void main(String[] args) {
class InnerBase{ //定义局部类
int a;
}
class InnerSub extends InnerBase{ //创建子类
int b;
}
InnerSub is = new InnerSub();
is.a = 5;
is.b = 8;
System.out.println(is.a + ";" + is.b); //5;8
}
}
编译上面的程序,会生成Test.class、Test$1InnerBase.class和Test$1InnerSub.class,注意,局部内部类的class文件名比成员内部类的class文件的文件名多了一个数字,这是因为同一个类中不可能有两个同名的成员内部类,但同一个类中可能有两个以上的同名局部内部类(处于不同方法中),所以加了个数字作为区分。
6.5 匿名内部类
匿名内部类适合哪种只需要一次使用的类,创建匿名内部类时会立即创建该类的一个实例,这个类定义立即消失,匿名内部类不能重复使用。
new 父类构造器(实参列表) | 实现接口(){
//类体
}
- 匿名内部类必须继承一个父类或实现一个接口
- 匿名内部类不能是抽象类,因为他要立即创建对象
- 匿名内部类不能定义构造器,因为他没有类名,无法定义,但它可以定义实例初始化块,来完成构造器的工作
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象
interface Product{
public double getPrice();
public String getName();
}
public class Test{
public void test(Product p){
System.out.println(p.getName() + ";" + p.getPrice());
}
public static void main(String[] args) {
Test t = new Test();
t.test(new Product() {
@Override
public double getPrice() {
return 2.14;
}
@Override
public String getName() {
return "present";
}
});
}
}
6.6 闭包和回调
闭包(Closure)是一种能被调用的对象,它保存了创建它的作用域的信息。
7、枚举类
在某些情况下,我们希望一个类的对象是有限而固定的,例如四季类,只有四个对象,这种类称为枚举类。
7.1 手动实现枚举类
手动实现枚举类,可以采用如下方式:
- 通过private将构造器隐藏起来
- 将这个类的所有可能实例都是用public static final修饰的类变量来保存
- 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例
class Season{
// 将成员变量和Season类定义为不可变
private final String name;
private Season(String name){ //private构造器
this.name = name;
}
public static final Season SPRING_SEASON = new Season("春天");
public static final Season SUMMER_SEASON = new Season("夏天");
public static final Season FALL_SEASON = new Season("秋天");
public static final Season WINTER_SEASON = new Season("冬天");
// 只为name提供getter方法
public String getName(){
return this.name;
}
}
4个static final常量代表了该类所能创建的对象,使用枚举类可以使程序更加简装,避免创建对象的随意性。
更早以前,程序员喜欢使用简单的静态常量来表示这种情况。
public static final int SPRING_SEASON = 1;
public static final int SUMMER_SEASON = 2;
public static final int FALL_SEASON = 3;
public static final int WINTER_SEASON = 4;
但是,使用这种方式有几个问题:
- 类型不安全,实际上他们都是整数,可以进行加法运算,这种代码是正确的,但是不符合语义
- 打印输出的意义不明确,如打印SPRING_SEASON,输出个1,很难联想到是春天
7.2 枚举类入门
枚举类有存在的价格,但是手动定义枚举类的代码了太大,所以JDK 1.5新增了对枚举类的支持。
新增了一个enum关键字(地位等同于class、interface)用以定义枚举类。枚举类是一种特殊的类,但是它也有自己的成员变量、构造器、成员方法,也可以实现一个或多个接口。
它与普通类的区别:
- 枚举类默认继承了java.lang.Enum类,Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口
- 使用enum定义、费抽象的枚举类默认使用final修饰,因此枚举类不能派生子类
- 枚举类的构造器只能使用private访问控制符,若省略,则默认使用private
- 枚举类的所有实例必须在枚举类的第一行显示列出,否则这个枚举类永远不能产生实例。这些实例系统会自动添加public static final修饰,无须程序员显式添加。
public enum SeasonEnum{
SPRING,SUMMER,FALL,WINTER;
}
for(Enum e: SeasonEnum.values()){ //返回枚举类的所有实例
System.out.println(e);
/*SPRING
SUMMER
FALL
WINTER*/
}
7.3 枚举类的成员变量、方法和构造器
枚举类也有自己的成员变量、构造器、成员方法,也可以实现一个或多个接口。
enum Gender{
MALE("男"), FEMAL("女");
private final String name; //final不可变
private Gender(String name){ //枚举类的构造器只能用private修饰
this.name = name;
}
public String getName(){
return this.name;
}
}
Gender gender = Enum.valueOf(Gender.class, "FEMAL");
System.out.println(gender.getName()); //女
7.4 实现接口的枚举类
枚举类实现接口,可以和普通类一样。
当时枚举类也可以以一种特殊的方式实现接口,使得不同枚举值调用方法具有不同的行为方式。
interface GenderDesc{
public void info();
}
enum Gender implements GenderDesc{
MALE("男"){//匿名内部子类
public void info(){
System.out.println("我是男性");
}
},
FEMALE("女"){
public void info(){
System.out.println("我是女性");
}
};
private final String name; //final不可变
private Gender(String name){ //枚举类的构造器只能用private修饰
this.name = name;
}
public String getName(){
return this.name;
}
}
Gender gender = Enum.valueOf(Gender.class, "FEMALE");
System.out.println(gender.getName()); //女
gender.info(); //我是女性
编译上面的程序,可以看到生成了Gender.class、Gender$1.class和Gender$2.class三个文件。
7.5 包含抽象方法的枚举类
enum Operate{
PLUS(){
@Override
double eval(double x, double y) {
return x + y;
}
},
MINUS(){
@Override
double eval(double x, double y) {
return x - y;
}}; //枚举类的对象
abstract double eval(double x, double y); //枚举类的定义;
}
System.out.println(Operate.PLUS.eval(3, 4)); //7.0
System.out.println(Operate.MINUS.eval(3, 4)); //-1.0
上面程序编译为5个class文件,Operation.class和四个匿名内部子类分别对应一个。
8、类成员
类成员有五种,成员变量、成员方法、构造器、初始化块、内部类(包括接口、枚举),static可以修饰除了构造器外的。
8.1 理解类成员
类成员变量属于整个类,当系统第一次准备使用该类时,系统会为该类成员变量分配内存空间,类成员变量开始生效,直到该类被卸载,该类的类成员变量所占有的内存才被系统的垃圾回收机制收回。
8.2 单例类
大部分的时候,我们将类的构造器定义为public访问权限,允许任何类自由创建该类的对象。但是有时候我们不想要频繁的创建和销毁对象。例如系统中可能只有一个窗口管理器,只有一个数据库引擎访问点,不需要为这种类创建多个实例。
如果一个类始终只能创建一个实例,则这个类被称为单例类。
为了避免其他类自由地创建该类的实例,我们可以将类的构造方法用private修饰,根据良好封装的原则,一旦我们把该类的构造器隐藏起来,我们就需要提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰,因为调用该方法之前还不存在这个对象,因此调用该方法的不可能是对象,只能是类。
此外,还必须缓存已经创建的对象,否则无法保证只创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,因此该成员变量也必须为static修饰。
class Singleton{
private static Singleton instance; //缓存
private Singleton(){} //private构造器覆盖默认构造器
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest{
public static void main(String[] args){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
syso(s1 == s2); //true,两次访问的是同一个对象
}
}