一:内部类
内部类:在某些情况下,我们把一个类存放在另一个类的内部定义,这个定义在其他类内部的类就叫做内部类(嵌套类)。包含内部类的类叫外部类(宿主类)。
内部类成员可以直接访问外部类的私有数据,因为内部类被当成其他外部类成员,同一个类成员可以相互访问。但外部类不能访问内部类的属性。
内部类分为:非静态内部类,静态内部类,局部内部类,匿名内部类。
非静态内部类:
不使用static关键字修饰的成员内部类叫非静态内部类,反之叫静态内部类。
内部类可以使用private,protected,public等访问控制符修饰。
内部类可以是直接访问外部类的私有属性,是因为在非静态内部类对象里,保存了一个它寄存的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类的实例,而非静态内部类实例必须寄存在外部类实例里)。在非静态内部类方法中访问某个变量时,系统优秀在该方法内部查找,依次是内部类中查找,外部类中查找,如果还是没找到,则编译错误。
如果外部类属性,内部类属性与内部类里方法中的局部变量名相同,则可以通过this,外部类类名.this来进行区分。
非静态内部类可以访问外部类的private成员,但反过来就不行,非静态内部类的成员只在非静态内部类范围内可知的,并不能被外部类直接使用,如果外部类需要访问非静态内部类成员,则必须显示创建非静态内部类对象来调用访问其实例成员。
经典实例:
public class OutClass {
private int outNum = 10;
class InnerClass {
private int inNum = 5;
public void acessOutProp() {
System.out.println("外部类的outNum属性值=" + outNum);
}
}
public void accessInnerProp() {
// 访问内部类的实例属性,必须显示创建内部类对象
InnerClass ic = new InnerClass();
System.out.println("内部类的inNum属性值=" + ic.inNum);
ic.acessOutProp();
}
public static void main(String[] args) {
OutClass oc = new OutClass();
oc.accessInnerProp();
}
}
经典问题:非静态内部类对象和外部类对象的关系是怎么样的?
解答:非静态内部类对象必须寄存在外部类对象里,而外部类对象则不一定有非静态内部类对象寄存在其中。简单的说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里就不一定寄存了非静态内部类对象,因此外部类对象访问非静态内部类成员时,可能非静态内部类对象根本不存在,而非静态内部类对象访问外部类成员时,外部类对象一定是存在的。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量,创建对象实例等。
经典实例:
public class Test5 {
private class Inner{
}
public static void main(String[] args) {
new Inner();//代码编译错误,静态成员无法访问非静态成员。
}
}
public class Test5 {
private class InnerClass{
/*下面三个静态声明都将引发编译错误,非静态内部类不能有静态声明
static{
System.out.println("我是内部类中的静态初始化块");
}
private static int inNum;
private static void test(){
System.out.println("test方法");
}
}
}
总结:非静态内部类里不可以有静态初始化块,但可以有普通的初始化块,非静态内部类普通初始化块的作用和普通类相同的。
静态内部类:
使用static关键字修饰的成员内部类叫静态内部类。该内部类属于整个外部类,而不是单独属于外部类的某个对象。
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规定,所以静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
静态问题:为什么静态内部类实例方法也不访问外部类的实例属性呢?
解答:因为静态内部类是外部类的类相关,而不是外部类的对象相关,也就是说,静态内部类的对象不是寄存在外部类对象里,而是寄存在外部类的类本身中,也就是说,当静态内部类的对象存在时,并不存在一个被它寄存的外部类对象,静态内部类的对象里值有外部类的类引用,没有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员时,但找不到被寄存的外部类对象,这将引发错误。
经典实例:
public class Test5 {
private int outNum=5;
private static int outNum2=10;
static class StaticInner{
//静态内部类里可以包含静态成员
private static int age;
public void accessOutProp(){
//下面这个代码编译错误,静态内部类无法访问外部类的实例成员
System.out.println(outNum);
System.out.println("outNum2");
}
}
}
除上面介绍的之外,Java还允许在接口中定义内部类,接口中定义的内部类默认使用public static修饰,接口内部类只能是静态内部类。但是,定义接口内部类的意义不大,因为接口的作用是定义一个公共的规范(暴露出来供大家使用)。
内部类的使用:
外部类以外使用非静态内部类:
经典实例:
public class Test5 {
public static void main(String[] args) {
//Out.In in=new Out().new In("Java程序员");和下面的代码功能完全一样
Out.In in;
Out out=new Out();
in=out.new In("Android程序员");
}
}
class Out{
class In{
public In(String msg){
System.out.println(msg);
}
}
}
外部类以外使用静态内部类
经典实例:
public class Test5 {
public static void main(String[] args) {
//StaticOut.StaticIn in=new StaticOut.StaticIn();和下面的代码功能相同
StaticOut.StaticIn in;
in=new StaticOut.StaticIn();
}
}
class StaticOut{
static class StaticIn{
public StaticIn(){
System.out.println("静态内部类的构造器");
}
}
}
经典提问:既然内部类是外部类的成员,是否可以为外部类定义子类,在子类中再定义一个内部类来重写其父类中内部类?
解答:不可以!内部类的类名不再是简单的由内部类的类名组成,它实际上还是外部类名作为一个命名空间,作为内部类类名的限制,因此子类中的内部类和父类中的累不累不可能完全同名,即使二者所包含的内部类的类名相同,但因为他们所处的外部类空间不同,所以他们不肯能是同一个类,也就不可能重写。
局部内部类:
把一个内部类定义在外部类的方法中,这样的内部类就局部内部类。局部内部类只能在该方法中有效。局部内部类也不能使用访问控制符和static修饰符修饰。
经典实例:
public class Test5 {
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("InnerSub对象的a和b变量的值是:"+is.a+", "+is.b);
}
}
匿名内部类:适合用于创建那些仅需要一次使用的类。创建匿名内部类时会立即创建一个该类的实例。这个类定义立即消失,匿名内部类不能重复使用。
匿名内部类规则:
(1)匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建一个该类的实例。
(2)匿名内部类不能定义构造器,因为匿名内部类没有类名。匿名内部类可以定义初始化块。
经典实例:
public class Test5 {
public void test(Product p){
System.out.println("购买了一个"+p.getName()+",花掉了"+p.getPrice());
}
public static void main(String[] args) {
Test5 test=new Test5();
//调用test方法时,需要传入一个Product参数,此处传入其匿名实现类的实例
test.test(new Product() {
@Override
public double getPrice() {
// TODO Auto-generated method stub
return 500.0;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return "希捷500G硬盘";
}
});
}
}
interface Product{
public double getPrice();
public String getName();
}
二.闭包和回调:
Java并不能显式地支持闭包,但对非静态内部类而言,它不仅记录了其外部类的详细信息,还保留了一个创建非静态内部类对象的引用,并且可以直接调用外部类的private成员,可以把非静态内部类当成面向对象领域的闭包。
当某个方法一旦获得了内部类对象的应用后,就可以在合适时候反过来调用外部类实例的方法。所谓回调,就是是运行客户类通过内部类引用来调用其外部类的方法。
经典实例:
public class Test5 extends Programmer{
public Test5() {
super();
// TODO Auto-generated constructor stub
}
public Test5(String name) {
super(name);
// TODO Auto-generated constructor stub
}
private void teach(){
System.out.println(getName()+" 教师在讲台上讲课");
}
private class Closure implements Teachable{
/*
* 非静态内部类回调外部类实现work方法,非静态内部类引用的作用仅仅是
* 向客户类提供一个回调外部类的途径
*
*/
public void work(){
teach();
}
}
//返回一个非静态内部类引用,允许外部类通过该非静态内部类引用来回调外部类的方法
public Teachable getCallbackReference(){
return new Closure();
}
public static void main(String[] args) {
Test5 test=new Test5("屌丝");
test.work();
//表面上调的是Closure的work方法,实际还是回调Test5的work方法
test.getCallbackReference().work();
}
}
interface Teachable{
void work();
}
class Programmer{
private String name;
public Programmer() {
super();
// TODO Auto-generated constructor stub
}
public Programmer(String name) {
super();
this.name = name;
}
public void work(){
System.out.println(name+"在等下认真的工作");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
三:枚举类
枚举是一种特殊的类,它一样可以有自己的方法和属性,可以实现一个或多个接口,也可以定义自己的构造函数。
枚举类与普通类的区别:
(1)枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。
(2)枚举类的构造器只能使用private访问控制符,系统默认使用private修饰。
(3)枚举类的所有实例必须在枚举类中显示列出(枚举值直接用逗号隔开),否则这个枚举类将永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰。
(4)所有枚举类都提供了一个values方法,该方法可以很方便地遍历所有的枚举值。
简单的枚举类格式:
public enum SeasonEnum {
SPRING,SUMMER,FALL,WINTER;
}
枚举类的属性,方法和构造器
经典实例:
public enum Gender {
MALE,FEMALE;
private String name;
public void setName(String name){
switch (this) {
case MALE:
if(name.equals("男")){
this.name=name;
}else{
System.out.println("参数错误");
return;
}
break;
case FEMALE:
if(name.equals("女")){
this.name=name;
}else{
System.out.println("参数错误");
return;
}
break;
default:
break;
}
}
public String getName(){
return this.name;
}
}
public static void main(String[] args) {
Gender g=Enum.valueOf(Gender.class,"MALE");
g.setName("男");
System.out.println(g+"代表:"+g.getName());
//此时设置name属性时将会提示参数错误。
g.setName("女");
System.out.println(g+"代表:"+g.getName());
}
枚举的构造方法:
public enum Gender {
//此处的枚举值必须调用对应的构造函数来创建
MALE("男"),FEMALE("女");
private String name;
//枚举类的构造函数只能使用private修饰
private Gender(String name){
this.name=name;
}
public String getName(){
return this.name;
}
}
public static void main(String[] args) {
Gender g=Enum.valueOf(Gender.class,"MALE");
System.out.println(g.getName());//打印男
}
枚举类实现接口和继承抽象类的相关知识和普通类完全相同,此处就不再详细总结。
四:对象与垃圾回收
垃圾回收:当程序创建对象,数组等应用类型实体时,系统都会在堆内存中为之分配一块内存空间,对象就保存在这块内存空间里,当这块内存不再被任何引用变量引用时,这块内存空间就变成了垃圾,等待垃圾回收机制进行回收。
特点:
(1)垃圾回收机制只负责回收堆内存中对象,不会回收任何物理资源(比如数据库连接,网络IO等资源)
(2)程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候运行。当对象永久性地失去引用后,系统就会在适合的时候回收它所占的内存。
(3)垃圾回收机制回收任何对象之前,总会先调用它的finalize方法,该方法可能使该对象重新复活(让一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。
当一个对象在堆内存中运行时,可以将对象所处的状态分为一下三种:
(1)激活状态:当一个对象被创建后,有一个以上的引用变量引用它,则这个对象在程序中处于激活状态,程序可以通过引用变量来调用该对象的属性和方法。
(2)去活状态:如果程序中某个对象不再被任何引用变量引用它,它就进入了去活状态。在这个状态下,系统的垃圾回收机制准备回收该对象所占的内存,回收之前,系统会调用去活状态对象的finalize方法进行资源清理,如果系统在调用finalize方法重新让一个引用变量引用该对象,则这个对象会再次变为激活状态,否则该对象将进入死亡状态。
(3)当对象与所有引用的关联都被切断,其系统已经调用所有对象的finalize方法依然没有使该对象变为激活状态,这个对象将永久的失去引用,最后变为死亡状态,只有当对象处于死亡状态时,系统才会真正回收该对象所占的资源。
强制垃圾回收:
程序是无法精确控制Java垃圾回收的时间,但我们依然可以强制系统进行垃圾回收:只是这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收这个还是不确定。当使用强制系统垃圾回收后还是会有一点效果的。
强制垃圾回收:(1)调用System类的gc()静态方法:System.gc();
(2)调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc();
经典实例:
public class Test {
public static void main(String[] args) {
for(int i=0;i<3;i++){
new Test();
//下面两行diamond的作用完全相同,强制系统进行垃圾回收
//Runtime.getRuntime().gc();
System.gc();
}
}
public void finalize(){
System.out.println("系统正在处理Test兑现的的资源...");
}
}
注意:强制垃圾回收仅仅只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,只是通知垃圾回收机制,让其尽快的进行垃圾回收。
finalize方法详解:
当垃圾回收机制回收某个对象所占用的内存空间之前,通常要求程序调用适当的方法来清理资源,在没有明确指定资源清理的情况下,Java提供了默认机制来清理该对象的资源,这个方法总是finalize。该方法是定义在Object类的实例方法,方法原型:protected void finalize() throws Throwable。
任何Java类都可以覆盖Object类的finalize方法,在该方法中清理该对象占用的资源,如果程序终止之前始终没有进行垃圾回收,则不会调用失去引用对象的finalize方法来清理资源。垃圾回收机制什么时候调用对象的finalize方法是完全透明的,只有当程序认为需要更多额外的内存时,垃圾回收机制才会进行垃圾回收。因此,有可能出现,当某个失去引用的对象只占用了少量内存,而且系统没有产生严重的内存需求,因此垃圾回收机制并没有试图回收该对象所占用的资源,所以该对象的finalize方法也不会调用。
finalize方法特点:
(1)永远不要主动调用某个对象的finalize方法,该方法是由垃圾回收机制调用。
(2)finalize方法何时被调用,是否被调用具有不确定性,不要把finalize方法当成一定会执行的方法。
(3)当虚拟机执行去活对象的finalize方法时,可能使该对象或系统中其他对象重新变为激活状态。
(4)当虚拟机执行finalize方法时出现了异常,垃圾回收机制不会报告异常,程序继续执行。
经典实例:
public class Test {
private static Test test=null;
public static void main(String[] args) throws Exception {
new Test();
//通知系统进行垃圾回收
System.gc();
//让程序暂停2秒
Thread.sleep(2000);
test.testInfo();
}
public void finalize(){
//让test引用试图回到去活状态
test=this;
}
public void testInfo(){
System.out.println("测试资源清理的finalize方法");
}
}
如果把程序中的Thread.sleep(2000);注释掉,运行结果将报空指针异常。从这点可以看错出,当程序调用了System.gc()方法后,系统并没有立即执行垃圾回收,否则将会先执行finalize方法,也就不会报空指针异常。
为了避免这样的问题。System和Runtime类中都提供了一个叫runFinalization方法,该方法可以强制垃圾回收机制调用finalize方法。这样就避免了上面报空指针的错误。