疯狂java学习→面向对象
Java8 增强的包装类
所有引用类型的变量都继承了 Object类, 都可当成 Object 类型变量使用。但基本数据类型的变量就不可以,如果有个方法需要 Object 类型的参数,但实际需要的值却是2、3等数值,这可能就较难以处理。
为了解决8种基本数据类型的变量不能当成 Object 类型变量使用的问题,Java 提供了包装类。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
从上表可以看出,int、char有点例外之外,其他的基本数据类型对应的包装类都是将其首字母大写即可。
从JDK l.5之后便提供了自动装箱和自动拆箱功能。所谓自动装箱,就是可以把一个基本类型变量直接赋给对应的包装类变量 或者赋给 Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量) 自动拆箱则与之相反,允许直接把包装类对象直接赋给一个对应的基本类型变量。
下面程序示范了自动装箱和自动拆箱的用法。
public class AutoBoxingUnboxing
{
public static void main(String[] args)
{
// 直接把一个基本类型变量赋给Integer对象
Integer inObj = 5;
// 直接把一个boolean类型变量赋给一个Object类型的变量
Object boolObj = true;
// 直接把一个Integer对象赋给int类型的变量
int it = inObj;
if (boolObj instanceof Boolean)
{
// 先把Object对象强制类型转换为Boolean类型,再赋给boolean变量
boolean b = (Boolean)boolObj;
System.out.println(b);
}
}
}
注意:进行自动装箱和自动拆箱时必须注意类型匹配,例如 Integer 只能自动拆箱成int类型变量,不要试图拆箱成 boolean 类型变量,与之类似的是, int 类型变量只能自动装箱成 lnteger对象(即使赋给 Object 类型变量,那也只是利用 Java 的向上自动转型特性) ,不要试图装箱成 Boolean对象。
除此之外,包装类还可实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式。
➢利用包装类提供的parseXxx(String s)静态方法(除Character 之外的所有包装类都提供了该方法)。
➢利用包装类提供的valueOf(String s)静态方法。
String intStr = " 123 " ;
// 一个特定字符串转换成 int 变量
int itl = Integer.parselnt(intStr) ;
int it2 = Integer. valueOf(intStr);
如果希望把基本类型变量转换成字符串,还有一种更简单的方法:将基本类型变量和""进行连接运算,系统会自动将基本类型变量转换成字符串。例如下面代码。
// intStr 的值为 5"
String intStr = 5 + "";
包装类的实例可以与数值类型的值进行比较。
Integer a = Integer.valueOf(6) ;
//输出 true
System out println(6的包装类实例是否大于 5.0 + (a > 5.0));
两个包装类的实例进行比较:因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回 true。
System.out println( 比较2个包装类的实例是否相等
+ (Integer.valueOf(2) == Integer.valueOf(2))); // 输出 false
// 通过自动装箱,允许把基本类型值赋值给包装类实例
Integer ina = 2 ;
Integer inb = 2 ;
System out println( 两个2自动装箱后是否相等 " + (ina ==inb) ); //输出 true
Integer biga = 128;
Integer bigb = 128;
System.out.println( 两个 128 自动装箱后是否相等: " + (biga == bigb) ); //输出 fa1se
java.lang. Integer 类的源代码把 -128~127 之间的整数自动装箱成 Integer 实例,并放入了一个名cache 的数组中缓存起来,因此 -128~127之间的同一个整数自动装箱成 Integer 实例时,永远都是引用 cache 数组的同一个数组元素,所以它们全部相等,但每次把一个不在 128~127 范围内的整数自动装箱成 Integer 实例时,系统总是重新创建 Integer 实例,所以出现程序中的运行结果。
处理对象
打印对象和 toString 方法
先看下面程序。
class Person
{
private String name;
public Person(String name)
{
this.name = name;
}
}
public class PrintObject
{
public static void main(String[] args)
{
// 创建一个Person对象,将之赋给p变量
Person p = new Person("孙悟空");
// 打印p所引用的Person对象
System.out.println(p);
}
}
运行上面程序,看到如下运行结果:
Person@15db9742
System.out println()方法只能在控制台输出字符串。
Object 类提供的 toString()方法总是返回该对象实现类的"类名 aaa@qq.com+ hashCode 值"。
==和 equals 方法
当使用==来判断两个变量是否相等时,如果两个变量是基本类型变量 ,且都是数值类型(不一定要求数据类型严格相同) ,则只要两个变量的值相等,就将返回 true。
但对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回 true。不可用于比较类型上没有父 关系的两个对象下。面程序示范了使用来判断两种类型变量是否相等的结果。
public class EqualTest
{
public static void main(String[] args)
{
int it = 65;
float fl = 65.0f;
// 将输出true
System.out.println("65和65.0f是否相等?" + (it == fl));
char ch = 'A';
// 将输出true
System.out.println("65和'A'是否相等?" + (it == ch));
String str1 = new String("hello");
String str2 = new String("hello");
// 将输出false
//分别指向两个通过 new 关键字创建的 String 对象
System.out.println("str1和str2是否相等?"
+ (str1 == str2));
// 将输出true
//String重写的equals()方法是判断两个符串所包含的字符序列(两个对象的值)相等
System.out.println("str1是否equals str2?"
+ (str1.equals(str2)));
// 由于java.lang.String与EqualTest类没有继承关系,
// 所以下面语句导致编译错误
// System.out.println("hello" == new EqualTest());
}
}
“hello” 直接量和 newString(“hello”) 有什么区别呢?
当 Java 程序直接使用形如"hello" 的字符串直接量( 包括可以在编译时就计算出来的字符串值)时,JVM 将会使用常量池来管理这些字符串;当使用 new String(“hello”) 时,JVM 会先使用常量池来管理"hello" 直接量 ,再调用String 类的构造器来创建一个新的 String 对象,新创建的 String 对象被保存在堆内存中。换句话说,new String(“hello”) 共产生了两个字符串对象。
提示:常量池专门用于管理在编译时被确定并被保存在已编译的.class文件中的一些数据,它包括了关于类、方法、接口中的常量,还包括字符串常量。
下面程序示范了JVM 使用常量池管理字符串直接量的情形。
public class StringCompareTest
{
public static void main(String[] args)
{
// s1直接引用常量池中的"疯狂Java"
String s1 = "疯狂Java";
String s2 = "疯狂";
String s3 = "Java";
// s4后面的字符串值可以在编译时就确定下来
// s4直接引用常量池中的"疯狂Java"
String s4 = "疯狂" + "Java";
// s5后面的字符串值可以在编译时就确定下来
// s5直接引用常量池中的"疯狂Java"
String s5 = "疯" + "狂" + "Java";
// s6后面的字符串值不能在编译时就确定下来,
// 不能引用常量池中的字符串
String s6 = s2 + s3;
// 使用new调用构造器将会创建一个新的String对象,
// s7引用堆内存中新创建的String对象
String s7 = new String("疯狂Java");
System.out.println(s1 == s4); // 输出true
System.out.println(s1 == s5); // 输出true
System.out.println(s1 == s6); // 输出false
System.out.println(s1 == s7); // 输出false
}
}
JVM常量池保证相同的字符串直接量只有一个,不会产生多个副本。例子中的是s1、s4、s5 所引用的字符串可以在编译期就确定下来,因此它们都将引用常量池中的同一个字符串对象。
使用 new String()创建的字符串对象是运行时创建出来的,它被保存在运行时内存区(即堆内存)内,不会放入常量池中。
Object 默认提供的 equals() 只是比较对象的地址,即 Object 类的 equals()方法比较的结果与==运算符比较的结果完全相同。因此,在实际应用中常常需要重写 equals 方法,重写 equals 方法时,相等条件是由业务要求决定的,因此, equals()方法的实现也是由业务要求决定的。
类成员
理解类成员
当系统第一次准备使用该类时,系统会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,该类的类变量所占有的内存才被系统的垃圾回收机制回收,类变量生存范围几乎等同于该类的生存范围。当类初始化完成后,类变量也被初始化完成。
静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦该类初始化结束后,静态初始化块将永远不会获得执行的机会。
单例类
如果一个类始终只能创建一个实例,则这个类被称为单例类。
为了避免其他类*创建该类的实例,应该把该类的构造器使用 private 修饰,从而把该类的所有构造器隐藏起来。
根据良好封装的原则:一旦把该类的构造器隐藏起来,就需要提供public() 方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static 修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)
除此之外,该类还必须缓存己经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象 ,为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用 static 修饰。
基于上面的介绍,下面程序创建了一个单例类。
class Singleton
{
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 将构造器使用private修饰,隐藏该构造器
private Singleton(){}
// 提供一个静态方法,用于返回Singleton实例
// 该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
// 如果instance为null,表明还不曾创建Singleton对象
// 如果instance不为null,则表明已经创建了Singleton对象,
// 将不会重新创建新的实例
if (instance == null)
{
// 创建一个Singleton对象,并将其缓存起来
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest
{
public static void main(String[] args)
{
// 创建Singleton对象不能通过构造器,
// 只能通过getInstance方法来得到实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//两次产生的 Singleton 象实际上是同一个对象
System.out.println(s1 == s2); // 将输出true
}
}
final修饰符
final 变量获得初始值之后不能被重新赋值。
final成员变量
Java法规定:final 修饰的成员变量(包括实例变量和类变量)必须由程序员显式地指定初始值。
》类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
》实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值 而且只能在三个地方的其中之一指定。
实例变量不能在静态初始化块中指定初始值,因为静态初始化块是静态成员,不可访问非静态成员;类变量不能在普通初始化块中指定初始值,因为类变量在类初始化阶段己经被初始化了,普通初始化块不能对其重新赋值。
下面程序演示了 final 修饰成员变量的效果。
public class FinalVariableTest
{
// 定义成员变量时指定默认值,合法。
final int a = 6;
// 下面变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
// 既没有指定默认值,又没有在初始化块、构造器中指定初始值,
// 下面定义的ch实例变量是不合法的。
// final char ch;
// 初始化块,可对没有指定默认值的实例变量指定初始值
{
//在初始化块中为实例变量指定初始值,合法
str = "Hello";
// 定义a实例变量时已经指定了默认值,
// 不能为a重新赋值,因此下面赋值语句非法
// a = 9;
}
// 静态初始化块,可对没有指定默认值的类变量指定初始值
static
{
// 在静态初始化块中为类变量指定初始值,合法
d = 5.6;
}
// 构造器,可对既没有指定默认值、有没有在初始化块中
// 指定初始值的实例变量指定初始值
public FinalVariableTest()
{
// 如果在初始化块中已经对str指定了初始化值,
// 构造器中不能对final变量重新赋值,下面赋值语句非法
// str = "java";
c = 5;
}
public void changeFinal()
{
// 普通方法不能为final修饰的成员变量赋值
// d = 1.2;
// 不能在普通方法中为final成员变量指定初始值
// ch = 'a';
}
public static void main(String[] args)
{
FinalVariableTest ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(ft.d);
}
}
如果打算在构造器、初始化块中对 final 成员变量进行初始化,则不要在初始化之前直接访问 final成员变量;但 Java 又允许通过方法来访问 final 成员变量,此时会看到系统将 final 成员变量默认初始化为0(或’\u0000’、false或 null) 例如如下示例程序。
public class FinalErrorTest
{
// 定义一个final修饰的实例变量
// 系统不会对final成员变量进行默认初始化
final int age;
{
// age没有初始化,所以此处代码将引起错误。
// System.out.println(age);
printAge();
age = 6;
System.out.println(age);
}
public void printAge(){
System.out.println(age);
}
public static void main(String[] args)
{
new FinalErrorTest();
}
}
final局部变量
下面程序示范了 final 修饰局部变量、形参的情形。
public class FinalLocalVariableTest
{
public void test(final int a)
{
// 不能对final修饰的形参赋值,下面语句非法
// a = 5;
}
public static void main(String[] args)
{
// 定义final局部变量时指定默认值,则str变量无法重新赋值
final String str = "hello";
// 下面赋值语句非法
// str = "Java";
// 定义final局部变量时没有指定默认值,则d变量可被赋值一次
final double d;
// 第一次赋初始值,成功
d = 5.6;
// 对final变量重复赋值,下面语句非法
// d = 3.4;
}
}
在上面程序中还示范了 final 修饰形参的情形。因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此使用 final 修饰的形参不能被赋值。
final 修饰基本类型变量和引用类型变量的区别
当使用 final 修饰基本类型变量时,不能对基本类型变量重新赋值。但对于引用类型变量,它保存的仅仅是一个引用, fina 只保证这个引用类型变量所引用的地址不会改变 ,即一直引用同一个对象,但这个对象完全可以发生改变。
下面程序示范了 final 修饰数组和 Person 对象的情形。
class Person
{
private int age;
public Person(){}
// 有参数的构造器
public Person(int age)
{
this.age = age;
}
// 省略age的setter和getter方法
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
public class FinalReferenceTest
{
public static void main(String[] args)
{
// final修饰数组变量,iArr是一个引用变量
final int[] iArr = {5, 6, 12, 9};
System.out.println(Arrays.toString(iArr));
// 对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
// 对数组元素赋值,合法
iArr[2] = -8;
System.out.println(Arrays.toString(iArr));
// 下面语句对iArr重新赋值,非法
// iArr = null;
// final修饰Person变量,p是一个引用变量
final Person p = new Person(45);
// 改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());
// 下面语句对p重新赋值,非法
// p = null;
}
}
从上面程序中可以看出,使用 final 修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。例如上面iArr 变量所引用的数组对象, final 修饰后的 iArr变量不能被重新赋值,但iArr所引用数组的数组元素可以被改变。类似的是,p变量也使用了 final 修饰,表明p变量不能被重新赋值,但p变量所引用 Person 对象的成员变量的值可以被改变。
final 方法
fmal 修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面程序完全没有问题。
// final 修饰的方法只是不能被重写 完全可以被重载
public final void test() {)
public final void test(String arg) {)
final类
final 修饰的类不可以有子类
抽象类
抽象方法和抽象类
抽象方法和抽象类必须使用abstract 修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类可以没有抽象方法。
抽象方法和抽象类的规则如下。
➢抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。
➢抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
➢抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
➢含有抽象方法的类(包括直接定义了一个抽象方法:或继承了一个抽象父类,但没有完全实现父类包含的抽象方法:或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
注意:抽象方法和空方法体的方法不是同一个概念。例如,public abstract void test();是一个抽象方法,它根本没有方法体,即方法定义后面没有一对花括号;但public void test(){}方法是一个普通方法,它已经定义了方法体,只是方法体为空,即它的方法体什么也不做,因此这个方法不可使用abstract 来修饰。
下面定义 Shape 抽象类。
public abstract class Shape
{
{
System.out.println("执行Shape的初始化块...");
}
private String color;
// 定义一个计算周长的抽象方法
public abstract double calPerimeter();
// 定义一个返回形状的抽象方法
public abstract String getType();
// 定义Shape的构造器,该构造器并不是用于创建Shape对象,
// 而是用于被子类调用
public Shape(){}
public Shape(String color)
{
System.out.println("执行Shape的构造器...");
this.color = color;
}
// 省略color的setter和getter方法
public void setColor(String color)
{
this.color = color;
}
public String getColor()
{
return this.color;
}
}
下面定义三角形类,三角形类被定义成普通类,因此必须实现 Shape 类里的所有抽象方法。
public class Triangle extends Shape
{
// 定义三角形的三边
private double a;
private double b;
private double c;
public Triangle(String color , double a
, double b , double c)
{
super(color);
this.setSides(a , b , c);
}
public void setSides(double a , double b , double c)
{
if (a >= b + c || b >= a + c || c >= a + b)
{
System.out.println("三角形两边之和必须大于第三边");
return;
}
this.a = a;
this.b = b;
this.c = c;
}
// 重写Shape类的的计算周长的抽象方法
public double calPerimeter()
{
return a + b + c;
}
// 重写Shape类的的返回形状的抽象方法
public String getType()
{
return "三角形";
}
}
下面再定义 Circle 普通类, Circle 类也是 Shape 类的一个子类。
public class Circle extends Shape
{
private double radius;
public Circle(String color , double radius)
{
super(color);
this.radius = radius;
}
public void setRadius(double radius)
{
this.radius = radius;
}
// 重写Shape类的的计算周长的抽象方法
public double calPerimeter()
{
return 2 * Math.PI * radius;
}
// 重写Shape类的的返回形状的抽象方法
public String getType()
{
return getColor() + "圆形";
}
public static void main(String[] args)
{
Shape s1 = new Triangle("黑色" , 3 , 4, 5);
Shape s2 = new Circle("黄色" , 3);
System.out.println(s1.getType());
System.out.println(s1.calPerimeter());
System.out.println(s2.getType());
System.out.println(s2.calPerimeter());
}
}
当使用 abstract 修饰类时,表明这个类只能被继承;当使用 abstract 修饰方法时,表明这个方法必须由子类提供实现(即重写) ,而final 修饰的类不能被继承,final 修饰的方法不能被重写。因此 final 和abstract 永远不能同时使用。
除此之外,当使用 static 修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法,但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误)。因此 static 和abstract 不能同时修饰某个方法,即没有所谓的类抽象方法,但它们可以同时修饰内部类。
注意:abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法休,因此 abstract方法不能定义为 private访问权限,即 private和abstract不能同时修饰方法。
Java9 改进的接口
Java9 对接口进行了改进,允许在接口中定义默认方法和类方法,默认方法和类方法都可以提供方法实现,Java9 为接口增加了一种私有方法,私有方法也可提供方法实现。
Java 中接口的定义
接口定义的基本语法如下:
[修饰符] interface 接口名 extends 父接口1,父接口2. . .
{
零个到多个常量定义 .. .
零个到多个抽象方法定义.. .
零个到多个内部类、接口、枚举定义.. .
零个到多个私有方法、默认方法或类方法定义 .. .
}
修饰符可以是 public 或者省略。
一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义,接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
接口里定义的成员变量只能在定义时指定默认值。
接口里定义成员变量采用如下两行代码的结果完全一样。
//系统自动为接口里定义的成员变量增加 public static final 修饰符
int MAX_SIZE = 50;
public static final int MAX_SIZE = 50 ;
接口里的普通方法总是使用 public abstract 来修饰。
接口的继承
接口的继承和类继承不一样,接口完全支持多继承。
interface InterfaceA
{
int PROP_A = 5;
void testA();
}
interface InterfaceB
{
int PROP_B = 6;
void testB();
}
interface InterfaceC extends InterfaceA, InterfaceB
{
int PROP_C = 7;
void testC();
}
public class InterfaceExtendsTest
{
public static void main(String[] args)
{
System.out.println(InterfaceC.PROP_A);
System.out.println(InterfaceC.PROP_B);
System.out.println(InterfaceC.PROP_C);
}
}
上面程序中的 InterfaceC 接口继承了 InterfaceA 和InterfaceB,所InterfaceC 中获得了它们的常量,因此在main()方法中看到通过 InterfaceC 来访问 PROP_A、 PROP_B、 PROP_C 常量。
接口和抽象类
接口和抽象类很像,它们都具有如下特征。
➢接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
➢接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
除此之外,接口和抽象类在用法上也存在如下差别。
➢接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
➢接口里只能定义静态常量,不能定义普通成员变量:抽象类里则既可以定义普通成员变量,也可以定义静态常量。
➢接口里不包含构造器:抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
➢接口里不能包含初始化块:但抽象类则完全可以包含初始化块。
➢一个类最多只能有一个直接父类,包括抽象类:但一一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
内部类
》内部类比外部类可以多使用 个修饰符: private protected static 一一外部类不可以使用修饰符
〉非静态内部类不能拥有静态成员。
非静态内部类
因为内部类作为其外部类的成员,所以可以使用任意访问控制符如 private protected public 等修饰。
在非静态内部类里可以直接访问外部类的 private 成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。下面程序示范了这个规则。
public class Outer
{
private int outProp = 9;
class Inner
{
private int inProp = 5;
public void acessOuterProp()
{
// 非静态内部类可以直接访问外部类的private成员变量
System.out.println("外部类的outProp值:"
+ outProp);
}
}
public void accessInnerProp()
{
// 外部类不能直接访问非静态内部类的实例变量,
// 下面代码出现编译错误
// System.out.println("内部类的inProp值:" + inProp);
// 如需访问内部类的实例变量,必须显式创建内部类对象
System.out.println("内部类的inProp值:"
+ new Inner().inProp);
}
public static void main(String[] args)
{
// 执行下面代码,只创建了外部类对象,还未创建内部类对象
Outer out = new Outer(); //①
out.accessInnerProp();
}
}
根据静态成员不能访问非静态成员的规则, 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之不允许在外部类的静态成员中直接使用非静部类。如下程序所示。
public class StaticTest
{
// 定义一个非静态的内部类,是一个空类
private class In{}
// 外部类的静态方法
public static void main(String[] args)
{
// 下面代码引发编译异常,因为静态成员(main()方法)
// 无法访问非静态成员(In类)
new In();
}
}
Java 不允许在非静态内部类里定义静态成员 。下面程序示范了非静态内部类里包含静态成员将引发编译错误。
public class InnerNoStatic
{
private class InnerClass
{
/*
下面三个静态声明都将引发如下编译错误:
非静态内部类不能有静态声明
*/
static
{
System.out.println("==========");
}
private static int inProp;
private static void test(){}
}
}
注意:非静态内部类里不可以有静态初始化块,但可以包含普通初始化块
静态内部类
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员,即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。下面程序就演示了这条规则。
public class StaticInnerClassTest
{
private int prop1 = 5;
private static int prop2 = 9;
static class StaticInnerClass
{
// 静态内部类里可以包含静态成员
private static int age;
public void accessOuterProp()
{
// 下面代码出现错误:
// 静态内部类无法访问外部类的实例变量
// System.out.println(prop1);
// 下面代码正常
System.out.println(prop2);
}
}
}
静态内部类是外部类的 个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、 创建对象等。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。下面程序示范了这条规则。
public class AccessStaticInnerClass
{
static class StaticInnerClass
{
private static int prop1 = 5;
private int prop2 = 9;
}
public void accessInnerProp()
{
// System.out.println(prop1);
// 上面代码出现错误,应改为如下形式:
// 通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.prop1);
// System.out.println(prop2);
// 上面代码出现错误,应改为如下形式:
// 通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}
使用内部类
内部类完整的类名应该是OuterClass. InnerClass
创建非静态内部类实例的语法如下:
OuterInstance .new InnerConstructor()
class Out
{
// 定义一个内部类,不使用访问控制符,
// 即只有同一个包中其他类可访问该内部类
class In
{
public In(String msg)
{
System.out.println(msg);
}
}
}
public class CreateInnerInstance
{
public static void main(String[] args)
{
Out.In in = new Out().new In("测试信息");
/*
上面代码可改为如下三行代码:
使用OutterClass.InnerClass的形式定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例将寄存在该实例中
Out out = new Out();
通过外部类实例和new来调用内部类构造器创建非静态内部类实例
in = out.new In("测试信息");
*/
}
}
创建静态内部类实例的语法如下:
new OuterClass. InnerConstructor ()
class StaticOut
{
// 定义一个静态内部类,不使用访问控制符,
// 即同一个包中其他类可访问该内部类
static class StaticIn
{
public StaticIn()
{
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance
{
public static void main(String[] args)
{
StaticOut.StaticIn in = new StaticOut.StaticIn();
/*
上面代码可改为如下两行代码:
使用OutterClass.InnerClass的形式定义内部类变量
StaticOut.StaticIn in;
通过new来调用内部类构造器创建静态内部类实例
in = new StaticOut.StaticIn();
*/
}
}
局部内部类
局部内部类不能使用访问控制符和static 修饰符修饰。
如果需要用局部内部类定义变量 、创建实例或派生子类,那么都只能在局部内部类所在的方法内进行。
public class LocalInnerClass
{
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);
}
}
Java 改进的匿名内部类
定义匿名内部类的格式如下:
new 实现接口() |父类构造器(实参列表)
{
//匿名内部类的类体部分
}
从上面定义可以看出, 匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下程序所示。
interface Product
{
public double getPrice();
public String getName();
}
public class AnonymousTest
{
public void test(Product p)
{
System.out.println("购买了一个" + p.getName()
+ ",花掉了" + p.getPrice());
}
public static void main(String[] args)
{
AnonymousTest ta = new AnonymousTest();
// 调用test()方法时,需要传入一个Product参数,
// 此处传入其匿名实现类的实例
ta.test(new Product()
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
});
}
}
正如上面程序中看到的,定义匿名内部类无须 class 关键字,而是在定义匿名内部类时直接生成该匿名内部类的对象 。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有方法。如果有需要,也可以重写父类中的普通方法。
对于上面创建 Product 实现类对象的代码, 可以拆分成如下代码。
class AnonymousProduct implements Product
{
public double getPrice()
{
return 567.8;
}
public double getPrice()
{
return "AGP 显卡";
}
ta . test(new AnonymousProduct());
在Java8 之前, Java 要求被局部内部类、匿名内部类访问的局部变量必须使用 final 修饰,从 Java 8开始这个限制被取消了, Java 更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final 修饰 ,例如如下程序。
interface A
{
void test();
}
public class ATest
{
public static void main(String[] args)
{
int age = 8; // ①
// 下面代码将会导致编译错误
// 由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
// age = 2;
A a = new A()
{
public void test()
{
// 在Java 8以前下面语句将提示错误:age必须使用final修饰
// 从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量
System.out.println(age);
}
};
a.test();
}
}
提示:对于被匿名内部类访问的局部变量,可以用 final 修饰,也可以不用 final 修饰,但必须按照有 final修饰的方式来用一一也就是一次赋值后,以后不能重新赋值。
Java8 新增的 Lambda 表达式
Lambda 表达式是 Java8 的重要更新,也是一个被广大开发者期待己久的新特性。 Lambda 表达式支持将代码块作为方法参数,允许使用更简洁的代码来创建只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
Lambda 表达式入门
public class CommandTest
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array , new Command()
{
public void process(int[] target)
{
int sum = 0;
for (int tmp : target )
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
});
}
}
Lambda 表达式完全可用于简化创建匿名内部类对象,因此可将上面代码改为如下形式。
public class CommandTest2
{
public static void main(String[] args)
{
ProcessArray pa = new ProcessArray();
int[] array = {3, -4, 6, 4};
// 处理数组,具体处理行为取决于匿名内部类
pa.process(array , (int[] target)->{
int sum = 0;
for (int tmp : target )
{
sum += tmp;
}
System.out.println("数组元素的总和是:" + sum);
});
}
}
从上面程序中的代码可以看出,Lambda 表达式就相当于一个匿名方法。只是不需要 new XxxO{} 这种烦琐的代码,不需要指出重写的方法名字,也不需要给出重写的方法的返回值类型一一只要给出重写的方法括号以及括号里的形参列表即可。
Lambda 表达式由三部分组成。
➢形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。
➢箭头(->)。必须通过英文中画线和大于符号组成。
➢代码块。如果代码块只包含一条语句, Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda 代码块只有一条returm语句,甚至可以省略returm关键字。Lambda 表达式需要返回值,而它的代码块中仅有一条省略了returm 的语句,Lambda 表达式会自动返回这条语句的值。
Lambda 表达式与函数式接口
Lambda 表达式的目标类型必须是"函数式接口( functional interface ) 。函数式接口代表只包含一个抽象方法的接口 。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。
方法引用与构造器引用
方法引用和构造器引用都需要使用两个英文冒号。Lambda 表达式支持如表 6.2 所示的几种引用方式。
1.引用类方法
例如,定义了如下函数式接口。
interface Converter{
Iηteger convert(String from);
}
该函数式接口中包含一个convert()抽象方法,该方法负责将 String 参数转换为Integer ,下面代码使用Lambda 表达式来创建一个 Converter 对象。
//下面代码使用 Larnb da 表达式创建 Converter 对象
Converter converterl = from -> Integer.valueOf(from);
上面 Lambda 表达式的代码块只有一条语句,因此程序省略了该代码块的花括号;而且由于表达式所实现的 convert()方法需要返回值,因此 Lambda 表达式将会把这条代码的值作为返回值。
接下来程序就可以调用 converter 对象的 convert()方法将字符串转换为整数了,例如如下代码.
Integer val = converterl.convert("99");
System.out.println(val); //输出整数 99
上面 Lambda 表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换.
//方法引用代替 Larnbda 表达式:引用类方法
//函数式接口中被实现方法的全部参数传给该类方法作为参数
Converter converterl = Integer::valueOf;
- 引用特定对象的实例方法
- 引用某类对象的实例方法
interface MyTest{
String test(String a , int b , int c);
}
该函数式接口中包含 testO抽象方法,该方法负责根据 String 、int 、int三个参数生一个 String返回值。
//下面代码使用 Lambda 表达式 MyTest 对象
MyTest mt = (a , b , c) - > a.substring (b , c} ;
String str = mt . test( "Java 1 Love you" , 2 , 9) ;
System .out.println(str} ; 11 输出 va 1 Lo
可以使用如下方法引用进行替换。
// 方法引用代替 Lambda 表达式: 引用某类对象的实例方法
// 函数式接口中被实现方法的第一个参数作为调用者
// 后面的参数全部传给该方法作为参数
MyTest mt = String : :substring;
- 引用构造器
interface YourTest{
JFrame win(String title);
}
// 下面代码使用 Lambda 表达式创建 YourTest 对象
YourTest yt = (String a) - > new JFrame(a);
JFrame jf = yt win( 我的窗口 ") ;
System . out.println(jf) ;
可以使用如下构造器引用进行替换:
// 构造器引用代替 Lambda 表达式
//函数式接口中国被实现方法的全部参数传给该构造器作为参数
YourTest yt = JFrame :: new;
Lambda 表达式与匿名内部类的联系和区别
Lambda 表达式与匿名内部类存在如下相同点。
➢Lambda 表达式与匿名内部类-样,都可以直接访问“ffectiveley final” 的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
➢Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。
下面程序示范了 Lambda 表达式与匿名内部类的相似之处。
interface Displayable {
// 定义一个抽象方法和默认方法
void display() ;
default int add(int a I int b ){
return a + b ; }
}
public class LambdaAndlnner{
private int age = 12 ;
private static String name = 疯狂软件教育中心 "J
public void test(){
String book = 疯狂 Java 讲义 "J
Displayable dis = ()->{
//访问 "ef fectively final" 的局部变量
System. out. println ("book 局部变量为: " + book) ;
//访问外部类的实例变量和类变量
System.out.println(" 外部类的 age 实例变量为: " + age);
System.out.println(" 外部类的 name 类变量为: " + name);
};
dis . display ();
// 调用 dis 对象从接口中继承的 add() 方法
System.out.println(dis. add(3 , 5)) ; //①
}
public static vo id main(String[] args){
LambdaAndlnner lambda = new LambdaAndlnner();
lambda. test () ;}
}
与匿名内部类相似的是,由于 Lamda 表达式访问了 book 局部变量,因此该局部变量相当于有一个隐式的 final 修饰,因此同样不允许对 book 局部变量重新赋值。
当程序使用 Lambda 表达式创建了 Displayabl 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法,如上面程序中①号粗体字代码所示。
Lambda表达式与匿名内部类主要存在如下区别。
➢匿名内部类可以为任意接口创建实例-- 不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。
➢匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。
➢匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法:但Lambda 表达式的代码块不允许调用接口中定义的默认方法。
对于Lambda 表达式的代码块不允许调用接口中定义的默认方法的限制,可以尝试对上面的程序稍做修改,在Lambda表达式的代码块中增加如下一行:
//尝试调用接口中的默认方法,编译器会报错
System. out.println(add( 3 , 5)) ;
虽然 Lambda 表达式的目标类型: Displayable 中包含了 dd 方法,但 Lambd 表达式的代码块不允许调用这个方法:如果将上面的 Lambda 表达式改为匿名内部类的写法,当匿名内部类实现display()抽象方法时,则完全可以调用这个 add()方法.
推荐阅读