疯狂Java讲义(六)----第三部分

1.内部类

        大部分时候,类被定义成一个独立的程序单元。在某些情况下,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类(有的地方也叫嵌套类),包含内部类的类也被称为外部类(有的地方也叫宿主类)。Java 从JDK 1.1开始引入内部类,内部类主要有如下作用。

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建Cow类,Cow类需要组合一个CowLeg对象,CowLeg类只有在Cow类里才有效,离开了Cow类之后没有任何意义。在这种情况下,就可把CowLeg定义成Cow的内部类,不允许其他类访问CowLeg。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
  • 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入一个Command对象时,重新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,因为这两个实现类可能仅需要使用一次。在这种情况下,使用匿名内部类将更方便。

        从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别。

  • 内部类比外部类可以多使用三个修饰符: private、protected、static——外部类不可以使用这三个修饰符
  • 非静态内部类不能拥有静态成员
     

  (1)非静态内部类

        定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。内部类定义语法格式如下:

         大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员:局部内部类和匿名内部类则不是类成员
        成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。

        上面程序中粗体字部分是一个普通的类定义,但因为把这个类定义放在了另一个类的内部,所以它就成了一个内部类,可以使用private修饰符来修饰这个类。
        外部类Cow里包含了一个test()方法,该方法里创建了一个CowLeg对象,并调用该对象的 info()方法。读者不难发现,在外部类里使用非静态内部类时,与平时使用普通类并没有太大的区别。

编译上面程序,看到在文件所在路径生成了两个 class文件,一个是Cow.class,另一个是Cow$CowLeg.class,前者是外部类Cow的class文件,后者是内部类CowLeg 的class文件,即成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形 式:OuterClass$InnerClass.class。
 

        当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果依然不存在,系统将出现编译错误:提示找不到该变量。
        因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this外部类类名.this作为限定来区分。如下程序所示。

 非静态内部类的成员可以访问外部类的 private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。下面程序示范了这个规则。

注意:

1.不允许在外部类的静态成员中直接使用非静态内部类。如下程序所示。


2.非静态内部类里不能有静态方法、静态成员变量、静态初始化块,所以上面三个静态声明都会引发错误。

   (2)静态内部类

        如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。

         静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。下面程序就演示了这条规则。        

         静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等
        外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。下面程序示范了这条规则。        

         除此之外,Java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类。
        如果为接口内部类指定访问控制符,则只能指定public访问控制符;如果定义接口内部类时省略访问控制符,则该内部类默认是public 访问控制权限。

2.使用内部类

        定义类的主要作用就是定义变量、创建实例和作为父类被继承。定义内部类的主要作用也如此,但使用内部类定义变量和创建实例则与外部类存在一些小小的差异。下面分三种情况讨论内部类的用法。

        1.在外部类内部使用内部类
        从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。
        唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员
        在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。

        2.在外部类以外使用非静态内部类
        如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用。

  • 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。
  • 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
  • 使用public修饰的内部类,可以在任何地方被访问。

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

        上面程序中粗体代码行创建了一个非静态内部类的对象。从上面代码可以看出,非静态内部类的构造器必须使用外部类对象来调用
        如果需要在外部类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。

        当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。下面程序定义了一个子类继承了Out类的非静态内部类ln类。

        非静态内部类In对象和SubClass对象都必须持有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。

        3.在外部类以外使用静态内部类
        因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:

 

        从上面代码中可以看出,不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
        因为调用静态内部类的构造器时无须使用外部类对象,所以创建静态内部类的子类也比较简单,下面代码就为静态内部类StaticIn类定义了一个空的子类。

 从上面代码中可以看出,当定义一个静态内部类时,其外部类非常像一个包空间

3.局部内部类

        如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。

         编译上面程序,看到生成了三个class文件:LocalInnerClass.class、LocalInnerClass$1InnerBase.class和LocalInnerClass$1InnerSub.class,这表明局部内部类的 class 文件总是遵循如下命名格式:OuterClass$NInnerClass.class。注意到局部内部类的class文件的文件名比成员内部类的class文件的文件名多了一个数字这是因为同一个类里不可能有两个同名的成员内部类,而同一个类里则可能有两个以上同名的局部内部类(处于不同方法中),所以Java为局部内部类的class 文件名中增加了一个数字,用于区分。

 4. Java 8  改进的匿名内部类

       匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时所需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用
        定义匿名内部类的格式如下:

         从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口
        关于匿名内部类还有如下两条规则。

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
  • 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下程序所示。

 Android按钮单击事件用到过

 在Java 8之前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰,从Java 8开始这个限制被取消了,Java 8更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了final 修饰。例如如下程序

 

         由于程序中①号代码定义age局部变量时指定了初始值,而上面代码再次对age变量赋值,这会导致Java 8无法自动使用final修饰age局部变量,因此编译器将会报错:被匿名内部类访问的局部变量必须使用final修饰

5. Java 8 新增的 Lambda 表达式

        Lambda表达式是Java 8的重要更新,也是一个被广大开发者期待已久的新特性。Lambda表达式支持将代码块作为方法参数, Lambda表达式允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。

我们来看一下变化

         从上面程序中的粗体字代码可以看出,这段粗体字代码与创建匿名内部类时需要实现的process(int[]target)方法完全相同,只是不需要new Xxx()这种烦琐的代码不需要指出重写的方法名字也不需要给出重写的方法的返回值类型——只要给出重写的方法括号以及括号里的形参列表即可
        从上面介绍可以看出,当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。

        从上面语法格式可以看出,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。它由三部分组成。

  • 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
  • 箭头(->)。必须通过英文中画线和大于符号组成。
  • 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return 的语句,Lambda表达式会自动返回这条语句的值。

         上面程序中的第一段粗体字代码使用Lambda表达式相当于不带形参的匿名方法,由于该Lambda表达式的代码块只有一行代码,因此可以省略代码块的花括号;第二段粗体字代码使用Lambda表达式相当于只带一个形参的匿名方法,由于该Lambda表达式的形参列表只有一个形参,因此省略了形参列表的圆括号;第三段粗体字代码的Lambda表达式的代码块中只有一行语句,这行语句的返回值将作为该代码块的返回值。
        上面程序中的第一处粗体字代码调用eat()方法,调用该方法需要一个Eatable类型的参数,但实际传入的是Lambda表达式;第二处粗体字代码调用drive()方法,调用该方法需要一个Flyable类型的参数,但实际传入的是Lambda表达式;第三处粗体字代码调用test()方法,调用该方法需要一个Addable类型的参数,但实际传入的是Lambda表达式。但上面程序可以正常编译、运行,这说明Lambda表达式实际上将会被当成一个“任意类型”的对象,到底需要当成何种类型的对象,这取决于运行环境的需要。下面将详细介绍Lambda表达式被当成何种对象。
 

6. Lambda表达式与函数式接口

Lambda表达式的类型,也被称为“目标类型( target type)",Lambda表达式的目标类型必须是“函数式接口( functional interface)”函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法


         从上面粗体字代码可以看出,Lambda表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着Lambda表达式有如下两个限制。

  • Lambda表达式的目标类型必须是明确的函数式接口
  • Lambda表达式只能为函数式接口创建对象。Lambda表达式只能实现一个方法,因此它只能为只有一个抽象方法的接口(函数式接口)创建对象

         为了保证Lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式。

  • 将Lambda表达式赋值给函数式接口类型的变量
  • 将Lambda表达式作为函数式接口类型的参数传给某个方法。
  • 使用函数式接口对Lambda表达式进行强制类型转换

因此,只要将上面代码改为如下形式即可(程序清单同上)。

7.方法引用于构造器引用

        前面已经介绍过,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用构造器引用
        方法引用和构造器引用可以让 Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如表6.2所示的几种引用方式。

  (1)引用类方法

        该函数式接口中包含一个convert()抽象方法,该方法负责将String参数转换为Integer。下面代码使用Lambda表达式来创建一个Converter对象(程序清单同上)。

        上面代码调用converter1对象的conver()方法时——由于converter1对象是Lambda表达式创建的,convert()方法执行体就是Lambda表达式的代码块部分,因此上面程序输出99。
        上面Lambda表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换(程序清单同上)。

  (2)引用特定对象的实例方法

 对于上面的实例方法引用,也就是调用"fkit.org"对象的 indexOf()实例方法来实现Converter 函数式接口中唯一的抽象方法,当调用Converter 接口中的唯一的抽象方法时,调用参数将会传给"fkit.org"对象的 indexOf()实例方法。

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

        该函数式接口中包含一个test()抽象方法,该方法负责根据String、int、int 三个参数生成一个String返回值。下面代码使用Lambda表达式来创建一个MyTest对象(程序清单同上)。

 对于上面的实例方法引用,也就是调用某个String对象的 substring()实例方法来实现 MyTest 函数式接口中唯一的抽象方法,当调用MyTest接口中的唯一的抽象方法时,第一个调用参数将作为substring()方法的调用者,剩下的调用参数会作为substring()实例方法的调用参数。

  (4) 引用构造器

        由于表达式所实现的win()方法需要返回值,因此Lambda表达式将会把这条代码的值作为返回值。
        接下来程序就可以调用yt对象的win()方法了,例如如下代码(程序清单同上);

 上面代码调用yt对象的win()方法时——由于yt对象是Lambda表达式创建的,因此 win()方法执行体就是Lambda表达式的代码块部分,即执行体就是执行new JFrame(a);语句,并将这条语句的值作为方法的返回值。

        对于上面的构造器引用,也就是调用某个JFrame类的构造器来实现 YourTest 函数式接口中唯一的抽象方法,当调用YourTest接口中的唯一的抽象方法时,调用参数将会传给JFrame构造器。从上面程序中可以看出,调用YourTest对象的 win()抽象方法时,实际只传入了一个String类型的参数,这个String类型的参数会被传给JFrame构造器——这就确定了是调用JFrame类的、带一个String参数的构造器
 

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

相同点:

  • Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
  • Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

 当程序使用Lambda 表达式创建了Displayable的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法,如上面程序中①号粗体字代码所示。

不同点:

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda表达式只能为函数式接口创建实例。
  • 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。
     

       对于Lambda表达式的代码块不允许调用接口中定义的默认方法的限制,可以尝试对上面的LambdaAndInner.java程序稍做修改,Lambda表达式的代码块中增加如下一行:

        虽然Lambda表达式的目标类型:Displayable中包含了add()方法,但Lambda表达式的代码块不允许调用这个方法;如果将上面的Lambda表达式改为匿名内部类的写法,当匿名内部类实现 display()抽象方法时,则完全可以调用这个add)方法。

9. 使用Lambda表达式调用Arrays的类方法

       前面介绍 Array类的功能时已经提到,Arrays类的有些方法需要Comparator、XxxOperator、XxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays 的方法。例如如下程序。

         上面程序中的粗体字代码就是Lambda表达式,第一段粗体字代码的Lambda表达式的目标类型是Comparator,该Comparator指定了判断字符串大小的标准:字符串越长,即可认为该字符串越大;第二段粗体字代码的Lambda表达式的目标类型是IntBinaryOperator,该对象将会根据前后两个元素来计算当前元素的值;第三段粗体字代码的Lambda表达式的目标类型是IntToLongFunction,该对象将会根据元素的索引来计算当前元素的值。编译、运行该程序,即可看到如下输出;

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/120911387