内部类
在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。
成员内部类
成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
class Circle {
private double radius = 0;
public static int count =1;
public Circle(double radius) {
this.radius = radius;
} class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
System.out.println(count); //外部类的静态成员
}
}
}
不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:
class Circle {
private double radius = 0; public Circle(double radius) {
this.radius = radius;
getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问
} private Draw getDrawInstance() {
return new Draw();
} class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
}
}
}
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:
public class Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建 //第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
} class Outter {
private Inner inner = null;
public Outter() { } public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
} class Inner {
public Inner() { }
}
}
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
另外,由于成员内部类可以访问类的成员,所以必须从类的实例才能创建成员内部类。
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
局部内部类定义在方法中,比方法的范围还小。是内部类中最少用到的一种类型。
像局部变量一样,不能被public, protected, private和static修饰。
只能访问方法中定义的final类型的局部变量。
局部内部类在方法中定义,所以只能在方法中使用,即只能在方法当中生成局部内部类的实例并且调用其方法。
我们来看一个例子:
public class Goods1 {
public Destination dest(String s) {
class GDestination implements Destination {
private String label; private GDestination(String whereTo) {
label = whereTo;
} public String readLabel() {
return label;
}
}
return new GDestination(s);
} public static void main(String[] args) {
Goods1 g = new Goods1();
Destination d = g.dest("Beijing");
}
}
在方法dest中我们定义了一个内部类,最后由这个方法返回这个内部类的对象。如果我们在用一个内部类的时候仅需要创建它的一个对象并创给外部,就可以这样做。当然,定义在方法中的内部类可以使设计多样化,用途绝不仅仅在这一点。
匿名内部类
匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段事件监听代码:
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub }
});
使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。
而内部类可以直接访问外部类的所有属性及方法(包括私有),所以使用匿名内部类可以实现其他语言的回调写法(如C#的委托或Lamdba,js的Function)。
final
Java8之前,匿名内部类只能访问使用final修饰的变量,这个限制在Java8中取消,下面我们来看看:
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
new Main();
} private ICallback callback; public Main()
{
test(100);
} private void test(final int a)
{
final String b = "abc"; callback = new ICallback()
{
@Override
public void Handler()
{
System.out.println("a: " + a + ", b: " + b);
}
}; callback.Handler();
}
} interface ICallback
{
void Handler();
}
Java8之前为什么需要添加final修饰呢?主要的问题是,Java8之前的版本中还没有很好的对闭包进行支持,由于局部变量的生命周期与局部内部类的对象的生命周期的不一致性,回调可能会在离开其访问的对象的作用域时调用,比如一个异步的操作,这样就会导致回调执行获取一个参数或临时变量时,该参数或变量已经被销毁的情况。
在最新的Java8中,已经支持闭包,对外部对象的访问不需要添加final修饰了。
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
我们都知道,Java中一个文件只能包含一个对外公开的类,且类名必须和文件名一致,那么如果我们需要再一个类中定义多个对外公开的类该怎么办?答案就是使用静态内部类。
如果使用过Google的Protobuf,就可以看见,生成的类文件大量的使用静态内部类来定义,避免创建类文件夹过多的问题。
下面我们来看一个例子:
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
Main.Test test = new Main.Test();
test.print();
} public static class Test
{
public void print()
{
System.out.println("Hello World!");
}
}
}
使用静态内部类我们可以看做定义了一个新类一样,仅仅在使用时需要先写外部类的名字,如例子中的“Main.Test”一样,实现了一个类文件定义多个公开类的功能。
内部类的优点
Java为什么要设计出内部类呢?
隐藏实现
对应成员内部类和局部内部类,通过在类中定义一个类来隐藏部分实现。
多重继承
对应成员内部类,比较另类,我就直接上代码了:
package test;
public class Class1 {
public String getName() {
return "Luck";
}
}
package test;
public class Class2 {
public int getAge() {
return 25;
}
}
package test;
public class MainClass {
public static class Test1 extends Class1 {
@Override
public String getName() {
return super.getName();
}
}
public static class Test2 extends Class2 {
@Override
public int getAge() {
return super.getAge();
}
}
public String showName() {
return new Test1().getName();
}
public int showAge() {
return new Test2().getAge();
}
public static void main(String args[]) {
MainClass example = new MainClass();
System.out.println("name:" + example.showName());
System.out.println("age:" + example.showAge());
}
}
实现Lamdba
对应匿名内部类,由于Java最小的单位是类,没有向C#一样提供委托,也没有向动态语言一样,最小单位是函数,如果希望编写一个事件监听,可以用到匿名内部类来实现。
定义多个类
对应静态内部类,Java不能在一个类中添加多个公开类,导致的问题是会产生多个类文件,如果希望向C++,C#一样可以在一个文件内添加多个类就要用到静态内部类了。
静态类
Java中唯一可以用static修饰的class只有上面的静态内部类,而普通的类是不能使用static修饰的,我们普遍说的Java静态类一般有下面三个特点:
- 使用final修饰,不允许继承;
- 只包含静态方法和属性;
- 构造函数私有化,不允许外部实例化;
而C#中是可以使用static修饰class的,其作用正好就是上面提到的三个特点;
静态类和单例的选择
- 单例可以被继承;
- 单例可以实现自某接口,可以继承自某类。静态类也可以继承自某类,但是就没法使用父类里面的protect成员了;
- 单例可以比较方便地扩展为有限实例;
- 如果希望在类加载的时候做复杂的操作,那么在静态类中,需要引入static块来初始化数据,如果期间抛出了异常,就可能发生一个“ClassDefNotFoundError”的诡异错误,这对问题定位是不利的;
好文推荐:
http://raychase.iteye.com/blog/1471015
http://www.2cto.com/kf/201311/260911.html
泛型
泛型在JDK1.5之后添加,泛型可以使类型参数化,从而实现了算法上的代码重用。
同时由于去掉了强制转换的操作,使用泛型还可以提高程序的运行速度。
我们先看看Java自带的使用了泛型的类:
package org.hammerc.study; import java.util.ArrayList;
import java.util.List; public class Main
{
public static void main(String[] args)
{
List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
int i = list1.get(0); List<String> list2 = new ArrayList<String>();
list2.add("Hello");
String s = list2.get(0);
}
}
通过使用泛型,我们可以重复利用ArrayList提供的功能,而不用每个类型对应去写一个ArrayList的类。
泛型在类上的实现
下面我们自己使用泛型编写一个简单的类,如下:
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
Test<Integer> test1 = new Test<Integer>();
test1.setMyValue(100);
System.out.println(test1.getMyValue()); Test<String> test2 = new Test<String>();
test2.setMyValue("Hello");
System.out.println(test2.getMyValue());
} public static class Test<T>
{
private T _myValue; public void setMyValue(T value)
{
_myValue = value;
} public T getMyValue()
{
return _myValue;
}
}
}
Test类中的尖括号里面的T即为泛型,其可以表示任意的类型。
泛型约束
我们上面示例中的T可以使用任意的类型,那么如果我们只希望T是某类型或某类型的子类该怎么办呢?
public class Test<T extends IComparable>
如果这样写,则表示T必须是实现了IComparable接口的对象。
多个类型的情况
多个类型的写法如下:
public class Test<T, K>
public class Test<T extends IComparable, K extends ICloneable>
如上所示,定义多个类型使用逗号分隔即可。
创建类型的情况
在Java中不能直接创建泛型,如下面的代码是错误的:
public class Test<T>
{
private T _test; public Test()
{
_test = new T();
}
}
因为对于编译器来说,不能确定创建的对象的参数,但是在C#中可以通过new()关键字指定该泛型一定存在一个无参构造函数,所以C#支持创建泛型对象。
设定为默认值
Java中,只有引用类型可以作为泛型的参数,int、boolean等基础类型不能作为泛型参数,所以设为默认值即置空即可。
泛型继承
子类也有相同的泛型时:
public class A<T>
{ } public class B<T> extends A<T>
{ }
当然,你可以使用另外的名称,只要能对应上即可:
public class A<T>
{ } public class B<K> extends A<K>
{ }
子类指定好类型:
public class A<T>
{ } public class B extends A<String>
{ }
子类添加新类型:
public class A<T>
{ } public class B<T, K> extends A<T>
{ }
泛型在方法上的实现
如果要在方法上添加类上没有指定的类型,可以直接在方法上添加泛型:
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
Test test = new Test();
String s = test.tell("Hello");
int i = test.tell(123);
} public static class Test
{
public <T> T tell(T t)
{
return t;
}
}
}
泛型接口
在JDK1.5之后,泛型也可以用在接口上,其使用方式和用在类上的使用方式一致,如下:
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
ITest test = new Test1<Integer>();
int i = (int) test.tell(100); //也可以写全, 外部使用不用转换
//ITest<Integer> test = new Test1<Integer>();
//int i = test.tell(100); ITest test2 = new Test2();
String s = (String) test2.tell("World"); //也可以写全, 外部使用不用转换
//ITest<String> test2 = new Test2();
//String s = test2.tell("World");
} public interface ITest<T>
{
T tell(T t);
} public static class Test1<T> implements ITest<T>
{
public T tell(T t)
{
return t;
}
} public static class Test2 implements ITest<String>
{
public String tell(String s)
{
return "Hello " + s;
}
}
}
泛型和静态字段与方法
泛型也可以用在静态方法上。
package org.hammerc.study; public class Main
{
public static void main(String[] args)
{
String s = Test.tell("Hello");
} public static class Test<T>
{
//public static T t;//报错,静态属性不能使用类上定义的泛型 //public static void func1(T t)//报错,静态方法不能使用类上定义的泛型
//{
//} public static <K> K tell(K k)
{
return k;
}
}
}
如上所示,静态属性和方法不能使用定义在类上的泛型;另外静态属性不能使用泛型。
通配符
我们先看下面的一种情况:
package org.hammerc.study; import java.util.ArrayList;
import java.util.List; public class Main
{
public static void main(String[] args)
{
List<A> a = new ArrayList<A>();
List<B> b = new ArrayList<B>(); func1(b);
func2(a);
} public static void func1(List<A> list) {}
public static void func2(List<B> list) {} public static class A {}
public static class B extends A {}
}
调用func1和func2的地方会报错,虽然B继承自A,但是List<A>和List<B>被编译器理解为两个类型,不能同意,再这种情况下就要使用到通配符了;
package org.hammerc.study; import java.util.ArrayList;
import java.util.List; public class Main
{
public static void main(String[] args)
{
List<A> a = new ArrayList<A>();
List<B> b = new ArrayList<B>(); func1(b);
func2(a);
} public static void func1(List<? extends A> list) {}
public static void func2(List<?> list) {} public static class A {}
public static class B extends A {}
}
使用“?”号表示匹配任意类型,使用“? extends A”表示匹配类型A即A的子类,这样修改后,调用就不会报错了;
上界和下界
Java中,使用通配符可以指定类型的上界和下界;
- 上界由extends指定:表示该类型必须是指定类型或其子类;
- 下界由super指定:表示该类型必须是指定类型或其父类;
一些要注意的地方
使用了通配符的List是不能进行添加操作的,但是可以添加null;
public static void func(List<?> list)
{
list.add(123);//报错,编译器不能得到实际的类型
list.add(null);//不报错
}
List<Object>与List<?>并不等同,不能往List<?> list里添加任意对象,除了null,但是List<Object>没有这个限制。
泛型数组
泛型数组是和泛型方法搭配使用的,具体的情况是在传递参数或返回值时使用,如下:
public class Main
{
public static void main(String[] args)
{
String[] s = {"a", "b", "c"};
tell(s); Integer[] i = {1, 2, 3};
tell(i);
} public static <T> T[] tell(T[] arr)
{
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
return arr;
}
}
和C#的对比
Java和C#的泛型实现是不一样的,具体的内容可以看下面这篇文章:
http://www.cnblogs.com/JeffreyZhao/archive/2010/02/22/why-not-csharp-on-jvm-type-erasure.html