论坛首页 Java企业应用论坛

Java获得泛型类型

浏览 55714 次
该帖已经被评为精华帖
作者 正文
   发表时间:2010-02-02   最后修改:2010-02-07
最近要想获得泛型类型,总结多方意见,再通过实践,最终获得了结果。
当然也被许多文章给误导过……
下面我们看一个例子,这个例子是我自己写的
/*
 * Copyright 2010 Sandy Zhang
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/**
 * 
 */
package org.javazone.jroi.test.reflect;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Sandy Zhang
 */
public class Bean
{
	public Map<String, ListBean> list = new HashMap<String, ListBean>();

	public static void main(String[] args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException
	{
		Field c = Bean.class.getField("list");
		Field f = Field.class.getDeclaredField("signature");
		f.setAccessible(true);
		System.out.println(((String) f.get(c)));

	}
}

这个只是我最近在写一个反射调用的东西想到的问题,所以很无奈才必须要这样得到东西
下面是结果
Ljava/util/Map<Ljava/lang/String;Lorg/javazone/jroi/test/reflect/ListBean;>;

字符串都有了,你怕什么呢?呵呵!

后话:这个方法并不应该被推荐,因为java api下并未提供任何方法实现,也就是说我们必须在特定环境和版本下才能这样干,至少得是1.5之后的版本以后sun(现在是oracle了)准备怎么改,这个是他的说法。。。
--------------------------------------------------------------------
OK,这个是一个引子,从这里我们看到了好些个玩意,也就是java其实提供了获取的方案的
那么我们要进行私有操作的,这样对我们写代码养成这样的习惯可不好,没事就去拿私有的东西,那是不是应该有公共的方法呢?我们试一下
将main里的调用修改一下
Field c = Bean.class.getField("list");
System.out.println(c.toGenericString());

public java.util.Map<java.lang.String, org.javazone.jroi.test.reflect.ListBean> org.javazone.jroi.test.reflect.Bean.list

看看,我们拿到了什么,不需要setAccess了,呵呵

哦,对了,你或许要问如果我不是字段呢?是方法怎么办。。于是乎
package org.javazone.jroi.test.reflect;

import java.util.HashMap;
import java.util.Map;

/**
 * @author Sandy Zhang
 */
public class Bean
{
	public Map<String, ListBean> list = new HashMap<String, ListBean>();

	public Map<String, ListBean> getList()
	{
		return list;
	}

	public static void main(String[] args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException
	{
		System.out.println(Bean.class.getMethod("getList").toGenericString());
	}
}


public java.util.Map<java.lang.String, org.javazone.jroi.test.reflect.ListBean> org.javazone.jroi.test.reflect.Bean.getList()

---------------------------------------------------------------------
字符串有了,我们考虑下,API里是不是应该也提供了方法直接获取泛型类型,参考下
getGenericType
public Type getGenericType()返回一个 Type 对象,它表示此 Field 对象所表示字段的声明类型。 
如果 Type 是一个参数化类型,则返回的 Type 对象必须准确地反映源代码中使用的实际类型参数。 
如果底层字段的类型是一个类型变量或者是一个参数化类型,则创建它。否则将解析它。 
返回:
返回表示此 Field 对象所表示字段的声明类型的 Type 对象 
抛出: 
GenericSignatureFormatError - 如果一般字段签名不符合 Java Virtual Machine Specification, 3rd edition 中指定的格式 
TypeNotPresentException - 如果底层字段的一般类型签名引用了不存在的类型声明 
MalformedParameterizedTypeException - 如果底层字段的一般签名引用了一个因某种原因而无法实例化的参数化类型
从以下版本开始: 
1.5 

由此可见,在1.5之后添加了众多以Generic为关键字的方法,这些方法就是用来获取泛型参数的有效途径,但是或许表面上看不出来,因为他们都是返回的Type接口,而非ParameterizedType接口,所以我困惑了很久。OK,下面我们看一下怎么实现吧
public class GenericTest
{
	public List<String> list = new LinkedList<String>();

	public static void main(String[] args) throws SecurityException, NoSuchFieldException
	{
		ParameterizedType pt = (ParameterizedType) GenericTest.class.getField(
				"list").getGenericType();
		System.out.println(pt.getActualTypeArguments().length);
		System.out.println(pt.getActualTypeArguments()[0]);
	}
}

1
class java.lang.String
这里是结果
---------------------------------------------------------------------
于是乎,所有的问题都迎刃而解了。

似乎文章很长,我通过Debug的方式发现了代码的运行过程,最终找到了核心字符串,说明即使是檫除式的泛型java也会记录下这些东西,没道理拿不到
   发表时间:2010-02-02  
额。。。怎么发到这里来了
0 请登录后投票
   发表时间:2010-02-02  
抄袭的吧?呵呵
0 请登录后投票
   发表时间:2010-02-02  
askyuan 写道
抄袭的吧?呵呵

为什么说我是抄袭?抄袭总要给个理由吧,你能在网上找到另外一篇么?至少最近不行,别人抄不抄就不知道了
0 请登录后投票
   发表时间:2010-02-02   最后修改:2010-02-02
askyuan 写道
抄袭的吧?呵呵

自己写不出来,就说人家抄袭,真卑鄙!真无耻!真龌龊!

楼主的这篇文章实在是太好了,让我如醍醐灌顶,茅塞顿开,解决了我多年来的疑惑!感谢楼主!
0 请登录后投票
   发表时间:2010-02-03  
Java泛型有这么一种规律:
位于声明一侧的,源码里写了什么到运行时就能看到什么;
位于使用一侧的,源码里写什么到运行时都没了。

什么意思呢?“声明一侧”包括泛型类型(泛型类与泛型接口)声明、带有泛型参数的方法和域的声明。注意局部变量的声明不算在内,那个属于“使用”一侧。
import java.util.List;
import java.util.Map;

public class GenericClass<T> {                // 1
    private List<T> list;                     // 2
    private Map<String, T> map;               // 3
    
    public <U> U genericMethod(Map<T, U> m) { // 4
        return null;
    }
}

上面代码里,带有注释的行里的泛型信息在运行时都还能获取到,原则是源码里写了什么运行时就能得到什么。针对1的GenericClass<T>,运行时通过Class.getTypeParameters()方法得到的数组可以获取那个“T”;同理,2的T、3的java.lang.String与T、4的T与U都可以获得。

这是因为从Java 5开始class文件的格式有了调整,规定这些泛型信息要写到class文件中。以上面的map为例,通过javap来看它的元数据可以看到记录了这样的信息:
private java.util.Map map;
  Signature: Ljava/util/Map;
  Signature: length = 0x2
   00 0A

乍一看,private java.util.Map map;不正好显示了它的泛型类型被擦除了么?
但仔细看会发现有两个Signature,下面的一个有两字节的数据,0x0A。到常量池找到0x0A对应的项,是:
const #10 = Asciz       Ljava/util/Map<Ljava/lang/String;TT;>;;

也就是内容为“Ljava/util/Map<Ljava/lang/String;TT;>;”的一个字符串。
根据Java 5开始的新class文件格式规范,方法与域的描述符增添了对泛型信息的记录,用一对尖括号包围泛型参数,其中普通的引用类型用“La/b/c/D;”的格式记录,未绑定值的泛型变量用“Txxx;”的格式记录,其中xxx就是源码中声明的泛型变量名。类型声明的泛型信息也以类似下面的方式记了下来:
public class GenericClass extends java.lang.Object
  Signature: length = 0x2
   00 12
// ...
const #18 = Asciz       <T:Ljava/lang/Object;>Ljava/lang/Object;;

详细信息请参考官方文档:http://java.sun.com/docs/books/jvms/second_edition/ClassFileFormat-Java5.pdf


相比之下,“使用一侧”的泛型信息则完全没有被保留下来,在Java源码编译到class文件后就确实丢失了。也就是说,在方法体内的泛型局部变量、泛型方法调用之类的泛型信息编译后都消失了。
import java.util.ArrayList;
import java.util.List;

public class TestClass {
    public static void main(String[] args) {
        List<String> list = null;       // 1
        list = new ArrayList<String>(); // 2
        for (int i = 0; i < 10; i++) ;
    }
}

上面代码中,1留下的痕迹是:main()方法的StackMapTable属性里可以看到:
StackMapTable: number_of_entries = 2
 frame_type = 253 /* append */
   offset_delta = 12
   locals = [ class java/util/List, int ]
 frame_type = 250 /* chop */
   offset_delta = 11

但这里是没有留下泛型信息的。这段代码只所以写了个空的for循环就是为了迫使javac生成那个StackMapTable,让1多留个影。
如果main()里用到了list的方法,那么那些方法调用点上也会留下1的痕迹,例如如果调用list.add("");,则会留下“java/util/List.add:(Ljava/lang/Object;)Z”这种记录。
2留下的是“java/util/ArrayList."<init>":()V”,同样也丢失了泛型信息。

由上述讨论可知,想对带有未绑定的泛型变量的泛型类型获取其实际类型是不现实的,因为class文件里根本没记录实际类型的信息。觉得这句话太拗口的话用例子来理解:要想对java.util.List<E>获取E的实际类型是不现实的,因为List.class文件里只记录了E,却没记录使用List<E>时E的实际类型。
想对局部变量等“使用一侧”的已绑定的泛型类型获取其实际类型也不现实,同样是因为class文件中根本没记录这个信息。例子直接看上面讲“使用一侧”的就可以了。

知道了什么信息有记录,什么信息没有记录之后,也就可以省点力气不去纠结“拿不到T的实际类型”、“建不出T类型的数组”之类的问题了orz
已被评为好帖!
   发表时间:2010-02-03  
不论是否为擦出式的,我们思考这样一个问题
static List<Object> list = new ArrayList<Object>();

public static void main(String[] args)
{
    list.add("asasdasd");
    list.add(123);
}

这样的定义,你怎么确定list里的类型?是Integer还是String?
这是一个问题,不知道C#里怎么实现的
0 请登录后投票
   发表时间:2010-02-03  
zcy860511 写道
不论是否为擦出式的,我们思考这样一个问题
static List<Object> list = new ArrayList<Object>();

public static void main(String[] args)
{
    list.add("asasdasd");
    list.add(123);
}

这样的定义,你怎么确定list里的类型?是Integer还是String?
这是一个问题,不知道C#里怎么实现的

很简单,首先看语言对泛型语义的要求,然后看实现方式。
从实现看,编译器中同一泛型参数被绑定为不同类型后生成的类型都被认为是一种独立的、新的类型;除非像Java一样在语言规范中规定已绑定参数的泛型类型与“raw type”之间有转换关系,否则新生成的类型之间就是不可转换的。具有膨胀法泛型语义的C++与C#中没有“raw type”的概念,一个模板(C++)或泛型类型(C#)必须等到其类型参数都绑定了之后才会得到类型的实例化,而只有实例化后的类型才可能有对象实例。即便是具有擦除法泛型语义的Java,要编译下面这段代码也是通不过的:
List<Exception> exceptions = new ArrayList<Exception>();
List<Error> errors = exceptions;

不兼容的类型
找到: java.util.List<java.lang.Exception>
需要: java.util.List<java.lang.Error>
        List<Error> errors = exceptions;
                             ^

要通过已绑定参数的泛型类型与raw type之间的转换关系来强制实现转换:
List<Exception> exceptions = new ArrayList<Exception>();
List<Error> errors = (List) exceptions;

这里想说明的是,即便是Java,编译器中也有泛型类型实例化的概念,只不过根据语言定义的转换允许泛型类型间的间接转换而已。


接着看C++:
vector<string> vec;

int main(int argc, char* argv[]) {
    vec.push_back("abcd"); // ok
    vec.push_back(123);    // compilation error
}

这里,vector不是一个类型,而是一个有待实例化的模板。其声明类似:
template < class T, class Allocator = allocator<T> >
class vector;

这里,T是必须赋值的类型参数,Allocator是有默认值的类型参数。“vector”自身不是完成类型,无法拥有对于的对象实例。假如让T与string绑定,形成vector<string>,这就是一个实例化了的模板类,或者说完成类型;它可以拥有对象实例,如上面代码中的vec。
形象点说,“vector”更像是一个工厂函数,自身并不是类型而可以生产出类型来。也可以看成是相对安全一些的宏,要把模板中的类型变量都替换为实际类型才能用于对象实例化。
由于C++的类型系统中模板类没有协变/逆变的概念,两个完成类型间无论如何都无法转换,除非动用reinterpret_cast<>。
这个转换通不过编译:
#include <vector>
using namespace std;

typedef long long int64;

int main(int argc, char* argv[]) {
    vector<double> ds;
    vector<int64>* ls = &ds;
}

而这个可以:
#include <vector>
using namespace std;

typedef long long int64;

int main(int argc, char* argv[]) {
    vector<double> ds;
    vector<int64>* ls = reinterpret_cast<vector<int64>*>(&ds);
}

但得知道自己到底在做什么就是了。
C++对RTTI的实现普遍比较弱,这里就不讨论运行时查询模板参数的问题了。
那么C++里为什么可以在模板里写“new T”?因为模板要实例化为完成类型才能用,此时T是已知的实际类型了;模板的实例化是在编译器中完成的。

然后看C#。
using System.Collections.Generic;

static class Program {
    static void Main(string[] args) {
        IList<string> list = new List<string>();
        list.Add("abc"); // ok
        list.Add(123);   // compilation error
    }
}

与C++类似,C#的泛型有膨胀式语义;泛型类型需要实例化为完成类型后才可以用于创建对象。上面的代码中,System.Collections.Generic.IList不存在,System.Collections.Generic.IList<>是未完成的泛型接口,System.Collections.Generic.IList<string>是一个完成的泛型接口。同样,System.Collections.Generic.List不存在,System.Collections.Generic.List<>是未完成的泛型类,System.Collections.Generic.List<string>是一个完成的泛型类。于是我们无法创建一个类型为System.Collections.Generic.List的对象实例,甚至不能写typeof(List)因为List不存在。List<>只能用在typeof()运算符当中,用于获取未完成泛型类型的反射信息。List<string>则是一个完成类型,可以用在任何普通类型可以用的地方,包括typeof(),包括new,等等。
在实现中,C#的编译器内部也会记录下泛型类型的类型实例化,并在生成的程序集(assembly)中保留相应的元数据(符号信息)。在运行时,CLI会读取这些元数据,在运行时对泛型类型做类型实例化,生成合适的RuntimeType来对应到每个实例化的泛型类型。通过反射可以在运行时获取任意对象的任意类型信息。上面的例子中可以在运行时通过反射查询到list指向的对象实例的类型是List<string>,通过该类型又可以查询到List<>与IList<string>、IList<>、string等类型。
与C++类似,C#中的泛型类型之间也无法随意转换,也没有“raw type”的概念。C# 4支持泛型的协变/逆变,允许诸如
IEnumerable<Apple> apples = GetApples();
IEnumerable<Fruit> fruits = apples;

的转换。

回到楼主的问题,如果有C#代码:
using System.Collections.Generic;

static class Program {
    static void Main(string[] args) {
        IList<object> list = new List<object>();
        list.Add("abc"); // ok
        list.Add(123);   // ok, int boxed
    }
}

那么list变量的类型是什么呢?是IList<object>,其泛型参数就是object,不是string也不是int。IList<object>、IList<string>、IList<int>之间没有转换关系,要转只有通过所有对象与object间的转换关系来间接强制转换,并在运行时得到无法转换的异常。
list所指向的对象实例是List<object>类型的,它的泛型参数也是object,而不是string也不是int。
14 请登录后投票
   发表时间:2010-02-03  
文明用语,别给中国人丢脸!
0 请登录后投票
   发表时间:2010-02-03  
andot 写道
askyuan 写道
抄袭的吧?呵呵

自己写不出来,就说人家抄袭,真卑鄙!真无耻!真龌龊!

楼主的这篇文章实在是太好了,让我如醍醐灌顶,茅塞顿开,解决了我多年来的疑惑!感谢楼主!



文明用语,别给中国人丢脸!
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics