java string转number_死磕Java之泛型(一)

死磕Java之泛型(一)

b58c975dce8c6d991032ceae3f6d58e2.png

一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类,如果需要编写可以应用于多种类型的代码,这种限制就降低了代码的可用性,当然你会想到重载,但是对于类呢,这就需要引入泛型了。

e15c6b8555416990a8dbc32677b45063.png

01泛型的基本概念

b58c975dce8c6d991032ceae3f6d58e2.png

泛型,从字面上理解就是适用于很多很多的类型,即参数化类型。从Java SE5开始,Sun公司就引入了泛型的概念。引入泛型的初衷是,希望类和方法具有更广泛的表达性!最直观的体现,就是我们现在所使用的容器类,例如List、Map、Set这些类;这些容器类的参数化类型表明了容器持有什么类型的对象,而且这些由编译器保证正确性。

在Java SE5之前,如果我们需要定义表某个类持有Object对象,我们可能编写如下的代码:

代码一:

class Apple{};
public class Hold2 {
    private Object obj;
    public Hold2(Object obj) {
        this.obj = obj;
    }

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
    //程序猿技术
public static void main(String[] args){
        Hold2 h=new Hold2(new Apple());
        Apple apple=(Apple)h.getObj();
        h.setObj("HelloWorld");
        String s=(String)h.getObj();
        h.setObj(1);
    }
}

这段代码可能存在的问题时类型转换异常,也许你对于这段简单的代码可保证不会出错,一旦逻辑复杂,并不能避免类型转换错误。

引入泛型之后,我们可以使用如下代码替换上面不安全的代码:

代码二:

public class Hold3<T> {
    private T obj;

    public Hold3(T obj) {
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
    //程序猿技术
public static void main(String[] args)
    {
        Hold3<Apple> hold3=new Hold3<Apple>(new Apple());
        Apple apple=hold3.getObj();
        //不能这样使用,因为固定了类型
        //hold3.setObj("ewdrf");
}
}

这里在类后面使用尖括号将参数类型包住,在使用的时候用实际参数替代T,而且也避免了类型转换异常。

e15c6b8555416990a8dbc32677b45063.png

02泛型的实现机制

b58c975dce8c6d991032ceae3f6d58e2.png

当你深入专研泛型时会发现,泛型是一种编译时多态。更加详细的来说,泛型的实现是在编译期间编译器对于参数类型的擦除。

我们现在回到上一节的代码二中,在Hold3类的编译期间,编译器会将泛型参数T擦除,取而代之的是Object插入到被擦除的地方。那么,是不是意味着如果我们不声明泛型,Hold3<T>和Hold3类是相同呢?

如果泛型参数没有限制,在大多数方面确实如此。我们可以在上一节main方法中给出如下的代码:

代码三:

/程序猿技术
public static void main(String[] args)
{
    Hold3<Apple> hold3=new Hold3<Apple>(new Apple());
    Hold3<Integer> hold=new Hold3<Integer>(new Integer(0));
    //答案是true
System.out.println(hold.getClass()==hold3.getClass());
}

这样看起来会非常奇怪,持有Apple的Hold3的Class对象居然和持有Integer的Hold3的Class对象相同。然而,如果你理解,擦除的含义,那么对于两者的Class相同并不会奇怪。因为在编译期间,对于实例化的参数,这里分别是Apple和Integer,编译器都是使用Object来替换,显然对于两个持有相同参数的Class表示为同一个Class。

如果你理解了擦除的真谛,那么下面的代码并不吃惊:

代码四:

//程序猿技术
public static void main(String[] args)
{
    List<String> stringList=new ArrayList<String>();
    List<Integer> integerList=new ArrayList<Integer>();
    System.out.println(stringList.getClass()==integerList.getClass());
}

这里依然是true。

e15c6b8555416990a8dbc32677b45063.png

03擦除的缺陷

b58c975dce8c6d991032ceae3f6d58e2.png

尽管擦除带来了很多便利之处,例如不需要强制转换、多态等,这也给程序员带来了困扰。比如在运行时,程序需要获取泛型声明的类型参数,具体代码如下:

代码五:

public static void main(String[] args) {
    List<String> stringList=new ArrayList<String>();
    Map<String,Integer> map=new HashMap<String,Integer>();
    //程序猿技术
System.out.println(Arrays.toString(stringList.getClass().getTypeParameters()));
    System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
}

与一般想法不一样的是,程序运行的结果如下:

f03caf7ac913cea2483f0f12b164adcc.png

可以看出仅仅只是输出了参数占位符的标识,这里有个残酷的现实:

在泛型代码内部,无法获得任何有关泛型参数类型的信息。

同样地,任何在运行时需要知道确切类型信息的操作都将无法工作。例如:

代码六:

public class Hold3<T> {
    private T obj;
    //编译错误
    //private T[] array=new T[1024];
    //private T t=new T();
public Hold3(T obj) {
        this.obj = obj;
    }

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }
    //程序猿技术
public static void main(String[] args)
    {

    }
}

上面代码中,我们无法使用T来创建数组和对象。显然,你无法确定用于替换T后的类型是否可以被实例化,或者有这样的构造器。如果代码中真的想要使用泛型数组,内部代码可以使用ArrayList替换一般的数组。同样,很多人喜欢使用在代码内部使用Class<T>,然后使用Class对象的方法newInstance()用于获取实例,需要注意的是,你无法确定替换泛型参数的类是否具有无参构造函数。在使用泛型时,你需要时刻替换自己,这个无界泛型参数在编译时用Object替换。

e15c6b8555416990a8dbc32677b45063.png

04边界与通配符

b58c975dce8c6d991032ceae3f6d58e2.png

原生类型即T,在编译期间被编译器被擦除后,被替换为Object。接下来我们将会看到一段令人费解的代码:

代码七:

public static void main(String[] args) {
    //程序猿技术
List<Number> numbers=new ArrayList<Number>();
  List<Integer> integers=new ArrayList<Integer>();
  //编译出错
  //numbers=integers;
    //numbers=new ArrayList<Integer>();
}

Number类是Integer类基类,按照一般的理解,numbers应该可以指向Integer的ArrayList。我们需要这样理解,numbers表示持有Number对象的容器,integers表示持有Integer对象的容器;numbers可以指向Number和其子类的容器,integers只想Integer及其子类的容器。诚然,numbers包含integers在内,但是它不是一个Integer的List,它仍然是Number的List。

其实,真正的问题是我们谈论的是容器,并不是容器持有的类型。编译时和运行时系统并不知道你想什么,以及类型的转换规则是怎么样的,所以如果让上面的代码合法,这里需要引入规则,告诉编译时和运行时系统应该遵守什么样的规则。

代码八:

public static void main(String[] args) {
    //程序猿技术
List<Number> numbers=new ArrayList<Number>();
  List<Integer> integers=new ArrayList<Integer>();
  //编译出错
  //numbers=integers;
    //numbers=new ArrayList<Integer>();

List<? extends Number>  arrays=new ArrayList<Integer>();
/**
arrays.add(10);
arrays.add(1L);
arrays.add(23.45f);
arrays.add('h');**/
}

这里,我们使用通配符?同时限制了arrays持有对象的上界,在擦除时,表示替换的必须是Number的子类。然而,新的问题以后来了,尽管这段代码是没有问题的,但是在向容器中添加元素时,你无法添加Number子类实例。这时你才发现并不如我们想的那样,即使添加Integer类,也无法添加。其实,我们无法确定arrays指向那个持有Number子类的容器,因为有可能我们指向持有Long的容器也有可能指向持有Double的容器,尽管在这两种类型可能不会产生类型转换异常,但是如果程序员不规范的使用,可能就会产生安全性问题。

那么,假设我们需要使容器添加Integer类型,我们应该如何做呢?这里就引入了超类型通配符,定义了下边界。

代码九:

List<? super Integer>  arrays=new ArrayList<Integer>();
arrays.add(10);
/**
arrays.add(1L);
arrays.add(23.45f);
arrays.add('h');**/

在更改之后,arrays容器中至少能添加Integer类型。但是对于Long,Doubler,Character等类型,依然不能添加,因为它们均不是Integer的类型。

e15c6b8555416990a8dbc32677b45063.png

a84e6ecedaf5046359fe88e998f68a14.png

6a53a385ad5dee2f42c93f1b44941d62.gif

点击上方二维码,关注我们

59ea8e1da5e56f671d39281162da7304.gif

15

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值