Java范型那些事(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/unicorn97/article/details/81813712

参考资料:http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

oracle官网介绍:https://docs.oracle.com/javase/tutorial/extra/generics/intro.html

同一系列:

Java范型那些事(二)

Java范型那些事(三)

 

在JDK1.5 加入了范型,范型可以增加代码的稳定性,使得在编译时能检查出更多的潜在bug,尤其是为集合类增加了编译时的类型安全,并减少了类型转换的苦差事。

目录

1. 简介

2. 定义简单的范型

3. 泛型和子类型

4. 通配符

5. 范型方法


1. 简介

泛型允许您抽象类型。最常见的示例是容器类型,例如Collections层次结构中的容器类型。

以下是此类的典型用法:

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3  

第3行的类型转换有点烦人。通常,程序员知道将哪种数据放入特定列表中。但是,类型转换是必不可少的。编译器只能保证迭代器返回一个Object。为确保赋值为Integer类型的变量是类型安全的,需要强制转换。

当然,类型强转不仅引入了混乱,它还引入了运行时错误的可能性,因为程序员可能会弄错。

如果程序员实际上可以表达他们的意图,并将列表标记为限制包含特定的数据类型,该怎么办?这是范型背后的核心理念。以下是使用泛型给出的上述程序片段的一个版本:

List<Integer> myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

注意变量myIntList的类型声明,它指定这不是一个任意List,而是一个数据类型是Integer的List,写成List <Integer>。我们说List是一个带有类型参数的通用接口 - 在本例中是Integer。我们还在创建列表对象时指定了类型参数。这时候,在取出元素时,不用在强制类型转化。

通过这种方式,编译器现在可以在编译时检查程序的类型正确性。当我们说使用类型List <Integer>声明myIntList时,这告诉我们关于变量myIntList的一些信息,无论何时何地使用它都保持类型不变,并且编译器将保证它。相比之下,强制类型转换只是程序员认为在某一时间点时是正确的。所以,特别是在大型程序中,使用范型是提高了可读性和稳健性。

 

2. 定义简单的范型

以下是java.util包中接口List和Iterator的定义的一小段摘录:


public interface  List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

在简介中,我们看到了泛型类型声明List的调用,例如List <Integer>。在调用(通常称为参数化类型)中,所有出现的形式类型参数(在本例中为E)都被实际类型参数(在本例中为Integer)替换。

⚠️注意:范型的声明类似于接口和类一样,都只有一个文件,不会增加多个代码副本。可以将类型参数理解成方法或者构造函数中的普通形式参数,在调用范型方法时,实际的类型参数将替换形式类型参数。

关于类型参数的命名,建议使用简洁的单个大写字符,在集合类中通常使用 E 来表示元素的类型,List <E>, Map中通常使用Map<K , V>  来表示,再次,请注意形式类型参数的命名约定 - 键为K,值为V

 

3. 泛型和子类型

通常,如果Foo是Bar的子类型(子类或子接口),并且G是一些泛型类型声明,则G <Foo>不是G <Bar>的子类型。这可能是您需要了解范型最难的事情,因为它违背了我们深刻的直觉。

 所以如果将子类型的范型对象赋值给父类型的范型对象,将会引起编译错误。

官网举了以下这个例子:

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 
lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

编译器会在其中第二行报错,解决办法见下一节 通配符

 

4. 通配符

Java中会遇到类似List< ? extends CustomClass>的上界通配符和List<? super CustomClass>的下界通配符,用以限定类型参数的范围,以下介绍以下为什么会要这样来做。

在JDK 1.5以前的集合遍历代码中,大概是以下样式:

void printCollection(Collection c) {
    Iterator i = c.iterator();
        for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
} 

在JDK1.5引入范型后,可以这样来写:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

但这并没有什么比旧版本好到哪去,因为旧版本中,可以使用任意的集合类型作为参数,新版本中,只能使用Collection<Object>作为参数,如上一节中所说,它不是各种集合的超类型!

那么各种集合类的超类型是什么?它写成Collection <?>(发音为“Collection of unknown”),即元素类型与任何东西匹配的集合。由于显而易见的原因,它被称为通配符类型。我们可以写:

void printCollection(Collection <?> c){
    for(Object e:c){
        System.out.println(E);
    }
}

现在,我们可以用任何类型的集合来调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并为它们指定类型Object。这总是安全的,因为无论集合的实际类型如何,它都包含对象。然而,向它添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

由于我们不知道c的元素类型代表什么,我们无法向其添加对象。 add()方法接受类型为E的参数,即集合的元素类型。当实际类型参数是?时,它代表某种未知类型。我们传递给add的任何参数都必须是这种未知类型的子类型。因为我们不知道它是什么类型,所以我们无法传递任何内容。唯一的例外是null,它是每种类型的成员。

另一方面,给定List <?>,我们可以调用get()并使用结果。结果类型是未知类型,但我们始终知道它是一个对象。因此,可以安全地将get()的结果赋给Object类型的变量,或者将其作为期望Object类型的参数传递。

但是在我们的项目中,经常需要限定该集合中可以存放哪些有某一共同特征的类的对象,但又不能使用List<Object>  或者List<?>, 于是就有了有界通配符,如:List <?extends Shape> shapes

其中的?代表一种未知的类型,就像我们之前看到的通配符一样。但是,在这种情况下,我们知道这种未知类型实际上是Shape的子类型。 (注意:它可能是Shape本身,或者是某些子类;它不需要字面上扩展Shape。)我们说Shape是通配符的上限

但是这里使用通配符后,在灵活性上却付出了代价,就是在方法体中添加这样的代码是非法的:

public void addRectangle(List <?extends Shape> shapes){
    //编译时错误!
    shapes.add(0,new Rectangle());
}

⚠️注意:您应该能够弄清楚为什么不允许上面的代码。 shapes.add()的第二个参数的类型是? extends Shape-- Shape的未知子类型。由于我们不知道它是什么类型,我们不知道它是否是Rectangle的超类型;它可能是也可能不是这样的超类型,所以在那里传递一个Rectangle是不安全的,但有界通配符却是可以解决传递进来的参数被限定为某些类型的解决方案。

 

5. 范型方法

先从一个例子说起,从一个数组中读取对象,存放到集合中,第一版代码如下:

static voidstatic void fromArrayToCollection(Object[] a, Collection<?> c) {
    for (Object o : a) { 
        c.add(o); // compile-time error
    }
}     

现在你应该不会像初学者一样,再使用 Collection<Object>作为集合的类型参数,但是使用Collection<?>同样也不行,如之前所说,unknown类型的集合,无法向其中添加元素。

这时候就可以使用范型方法来解决该问题,同范型类的声明一样,方法声明也可以是范型的,即使用一个或多个参数化的类型

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // Correct
    }
}

按照上面的范型方法,然后我们可以使用任何元素类型是数组元素类型的超类型的集合来调用该方法。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();

// T 被推断为 Object 类型
fromArrayToCollection(oa, co); 

String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();

// T 被推断为 String
fromArrayToCollection(sa, cs);

// T 被推断为 Object
fromArrayToCollection(sa, co);

Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();

// T 被推断为 Number
fromArrayToCollection(ia, cn);

// T 被推断为 Number
fromArrayToCollection(fa, cn);

// T 被推断为 Number
fromArrayToCollection(na, cn);

// T 被推断为 Object
fromArrayToCollection(na, co);

// 编译报错
fromArrayToCollection(na, cs);

⚠️注意:我们不必将实际类型参数传递给泛型方法。编译器根据实际参数的类型为我们推断类型参数。它通常会推断出使得调用时类型安全的最具体的类型参数。

出现的一个问题是:何时应该使用泛型方法,何时应该使用通配符类型?为了理解答案,我们来看一下Collection库中的一些方法。

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

我们也可以使用范型方法替代上面的代码:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // 嘿,类型变量也可以有界限!
}

但是,在containsAll和addAll中,类型参数T仅使用一次。返回类型不依赖于类型参数,也不依赖于方法的任何其他参数(在上述情况下,只有一个参数)。这告诉我们类型参数用于多态;它唯一的作用是允许在不同的调用站点使用各种实际的参数类型。如果是这种情况,则应使用通配符。通配符旨在支持灵活的子类型,这是我们在此尝试表达的内容。

范型方法允许使用类型参数来表示方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。如果没有这种依赖关系,则不应使用范型方法。

可以串联使用范型方法和通配符。这是方法Collections.copy():

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

注意两个参数类型之间的依赖关系。从源列表src复制的任何对象必须可分配给目标列表的元素类型T,dst。所以src的元素类型可以是T的任何子类型 - 我们不关心哪个。copy方法使用类型参数表示依赖关系,但对第二个参数的元素类型使用了通配符。

我们可以用另一种方式为这种方法编写签名,而不使用通配符:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

这很好,但是第一个类型参数T既在dst的类型中使用,也在第二个类型参数S的边界中使用,且S本身只使用一次(即在src的类型中 - 没有别的依赖它)。这表明我们可以用通配符替换S。使用通配符比声明明确的类型参数更清晰,更简洁,因此应尽可能优先使用。

通配符还具有以下优点:它们可以在方法签名之外使用,如字段类型,局部变量和数组。如:

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addLast(shapes);
    for (Shape s: shapes) {
        s.draw(this);
    }
}

最后,再次让我们注意用于类型参数的命名约定。我们使用T作为类型,只要没有更具体的类型来区分它。范型方法通常就是这种情况。如果有多个类型参数,我们可能会使用字母表中与T相邻的字母,例如S。如果泛型方法出现在泛型类中,最好避免对方法的类型参数使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。

展开阅读全文

Java 内存的那些

02-22

虽然Java屏蔽了一下内存细节,但是有时候,了解一下这些常识还是有好处的,特别是一些面试,总是盯着这些玩意不放手。把最近看的一些总结一下,欢迎跟帖拍砖。rnrnJVM启动以后,会分配两类内存区域,一类用于开发人员使用,比如保存一些变量,对象等,一类JVM自己使用,比如存放一些class类和描述。rnrn1,第一类内存区域又可以分为栈(stack)、堆(heap),还有一些静态存储区域,这部分的内存在JVM启动的时候,可以用参数进行配置:rnrn-Xms 初始堆大小,这个值不能太小,其初始空间(即-Xms)是物理内存的1/64,这个值不能太小,比如 设置了-Xms1m,运行可能会出现 rn Error occurred during initialization of VMrn Too small initial heap for new size specifiedrnrn-Xmx 堆大小上限,最大空间(-Xmx)是物理内存的1/4,如果程序中分配的内存超过了这个限制,那么会出现rnException in thread "main" java.lang.OutOfMemoryError: Java heap spacern 代码为:byte[] b = new byte[100000000];rnrn-Xss 线程栈大小,一般不用设置,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。有时候会发现一下异常,rn Exception in thread "main" java.lang.StackOverflowErrorrnrn 原因一般是:rnrn public static int callMyself()rn return callMyself();rn rnrn 方法的递归或者死循环,导致栈空间不够用了。rnrn 栈和堆到底存些什么,很多地方都有讲到,这里参考下《Think in java》的,栈里存放对象引用、基本类型的变量等,而堆里面存放对象和数组。方法的执行是在栈上进行的,这一点可以通过异常的时候,经常会默认打印e.printStackTrace();栈信息得知。rnrnrnRuntime类有几个函数,我们可以简单的通过这几个函数,看看JVM中的一些内存信息,下面是转自网络上的解释。rnrnmaxMemory()这个方法返回的是java虚拟机(这个进程)能构从操作系统那里挖到的最大的内存,以字节为单位,如果在运行java程序的时 候,没有添加-Xmx参数,那么就是64兆,也就是说maxMemory()返回的大约是64*1024*1024字节,这是java虚拟机默认情况下能 从操作系统那里挖到的最大的内存。如果添加了-Xmx参数,将以这个参数后面的值为准,例如java -cp ClassPath -Xmx512m ClassName,那么最大内存就是512*1024*0124字节。rnrntotalMemory()这个方法返回的是java虚拟机现在已经从操作系统那里挖过来的内存大小,也就是java虚拟机这个进程当时所占用的所有 内存。如果在运行java的时候没有添加-Xms参数,那么,在java程序运行的过程的,内存总是慢慢的从操作系统那里挖的,基本上是用多少挖多少,直 挖到maxMemory()为止,所以totalMemory()是慢慢增大的。如果用了-Xms参数,程序在启动的时候就会无条件的从操作系统中挖- Xms后面定义的内存数,然后在这些内存用的差不多的时候,再去挖。rnrnfreeMemory()是什么呢,刚才讲到如果在运行java的时候没有添加-Xms参数,那么,在java程序运行的过程的,内存总是慢慢的从操 作系统那里挖的,基本上是用多少挖多少,但是java虚拟机100%的情况下是会稍微多挖一点的,这些挖过来而又没有用上的内存,实际上就是 freeMemory(),所以freeMemory()的值一般情况下都是很小的,但是如果你在运行java程序的时候使用了-Xms,这个时候因为程 序在启动的时候就会无条件的从操作系统中挖-Xms后面定义的内存数,这个时候,挖过来的内存可能大部分没用上,所以这个时候freeMemory()可 能会有些大。rnrnrn下面我们来看看例子:rnrn Runtime rt = Runtime.getRuntime();rn rn info("Max memory: " + rt.maxMemory());rn long fisrt = rt.freeMemory();rn info("Total memory: " + rt.totalMemory());rn info("Free memory: " + fisrt);rn rn int size = 10000;rn rn byte[] b = new byte[size];rn long bL = rt.freeMemory();rn info("Free memory: " + bL);rn info("byte allocate Cost memory: " + (fisrt - bL) + ", Array size :" + size);rnrn 运行参数为 -Xms8m -Xmx32m (太大了可能看不出来),运行结果为:rn2011-02-22 10:28:01: Max memory: 33357824rn2011-02-22 10:28:01: Total memory: 8323072rn2011-02-22 10:28:01: Free memory: 7791752rn2011-02-22 10:28:01: Free memory: 7781736rn2011-02-22 10:28:01: byte allocate Cost memory: 10016, Array size :10000rnrn 33357824 <> 32*1025*1024(大约等于)rnrn 8323072 <> 8×1024×1024rnrn 最后看看10000长度的byte数组,分配了多少内存,大约为10016,这说明除了10000个大小为1字节的byte以外,还有16个字节其他的玩意。rnrn将byte换成int(4字节):rn2011-02-22 10:35:21: int allocate Cost memory: 40016, Array size :10000rn 与byte相同,也是4*10000+16rnrn将byte换成long(8字节):rn2011-02-22 10:32:47: long allocate Cost memory: 80016, Array size :10000rn与byte相同,也是8*10000+16rnrn再看看String数组:rn2011-02-22 10:34:40: String allocate Cost memory: 40016, Array size :10000rnString作为一个对象,分配的内存大小与int相同,说明了这台机器是32(4*8)位的rnrnrn最后看看Object对象,rn2011-02-22 10:37:02: Object allocate Cost memory: 40016, Array size :10000rn与String一样。rnrnrn2,第二类内存,我了解的主要是PermGen space,全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,SUN的JDK在GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很CLASS的话,就很可能出现PermGen space错误。rnrn 原来SUN 的JVM把内存分了不同的区,其中一个就是permenter区用来存放用得非常多的类和类描述。本来SUN设计的时候认为这个区域在JVM启动的时候就固定了,但他没有想到现在动态会用得这么广泛。而且这个区域有特殊的垃圾收回机制,现在的问题是动态加载类到这个区域后,gc根本没办法回收。rn rn Permgen space的参数为-XX:PermSize=128M -XX:MaxPermSize=512mrnrn 论坛

工作一年半那些

02-03

年末了,总会发现一些事情没有做,一些事情做得不够好,一些事情似乎没有必要去做。大约又是发现长了一岁,所以很多事情一考虑,就很头大。失眠啦,烦躁啦,什么的,哎。rnrn工作一年半,之前担心的事情,也渐渐清晰了,之前的价值观,现在也在渐渐改变,再固执的我,现在也发现年轻时候做了一些错事。现在回过头去看大学的那班孩子,总想说:瞧!那群SB。其实骂不是他们,而是骂的自己的过去。rnrn现在手头上的这份工作是大学期间找的第四份工作。由于挂科太多,技术底气不足(现在发现,只是我之前接触的高手太高),面的都是中小型企业。四份工作,一份外包不考虑,一份被刷,后来考虑呆在广州,这确定了现时这份工作。这家公司其实挺好,加班不多,一年时间,或许半年是在自我修行,纪律问题也很好说话,可惜氛围太差,不够聪明的人太多了。我对自己的定位是:我还是比较合适当凤尾,不合适当鸡头的人。9年义务教育,3年高中,4年大学,一路靠着自己的小聪明,和啃老的心态走了过来,发现自己在聪明人里,才会看到自己的差距,在一堆不怎么聪明的人里,总会得意忘了形。最近总想说,一念嗔心起,百万障门开,其实自我提醒就是这个。说到这个,我总想对高中一个同学说对不起,那会儿我骂他书呆子,他其实是个极其聪明的人,我嫉妒他了,现在只能望其项背了。顺带提一句,不知道现时持读书无用论的人还多不多,除去做生意或者拼爹的那些少数成功例子,其实你会发现,高等学府的人,无论如何,活得是比我这们这些二流大学出来的要好,至少在我的圈子是这样,一个氛围确实很重要,可是其实大家都打DOTA,是不是?rnrn其实,现在年纪越来越大,就越担心氛围对自己造成的影响,毕竟已经到了一个很容易形成定性思维的年纪了,除了想问题越来越绝对,发散思维也越来越差,记忆力也在衰减,回忆儿时的梦想,就会觉得越来越恐惧。哪一天,我要是没看代码就对着另外一名程序员说,你这样做不对。我觉得我就无法在技术这条道路上走下去了。现在的工作氛围就是我担心的事情,每每我判断一个人蠢不蠢,我总会用这样一个例子,那就是讨论int是多少字节的问题,虽然极少数人会讨论,或者讨论的人会就机器字数来讨论这个设计问题,但这就不是蠢问题了,蠢问题是他为什么不用sizeof(int)去验证,而要花一上午的时间来做这个无价值的事情。类似的事情很多,我就处在这个氛围之下,或多或少会被带进去。rnrn在这家公司里,我还是很佩服我的主管,他是个聪明人,教给我很多事情,例如从写学习型代码,转向写生产型代码。他虽然对各种技术不发烧,这限制了他的眼界,但是他考虑事情确实很周到,也很长远。如果他不一次又一次地将我的方案推倒,而是采用验证的思想证明我的方案是错误的,还有把我晾在项目外闲着,我其实挺喜欢这个人。这次年终总评,我发现我还是不错的,我不知道是不是其它人太差了,还是他其实还是欣赏我这个人的。当然,我也明白,他希望他手下的人能扎实完成工作,而不是提些有的没的。总之,我觉得他老了吧,大约。与之同时,一两个老兵,也教会了我代码设计上的一些东西,我也很感谢他们。rnrn除了氛围,一个就是远见的问题。我们这些当兵的混社会的,都希望跟着老大吃香喝辣,弟兄们把生命都交给你了,唯一的希望是你能负责。这也是我对公司的唯一要求。技术人的生命很短暂,过不了混饭的日子。我不知道公司到底有没有为我们员工设定了一个发展曲线,所以每当我一闲下来就很害怕。一闲下来,我就会拼命学习一些乱七八糟的东西,希望能跳出坑里,但我也明白营养不大。我背后有一个家庭,还有一堆琐碎的事,一些不通情达理的人,还有自己也不争气,我也知道自己在迂回战斗着,跑了很久,结果在原地打转。我渴望懂得更多东西,让自己变得更有价值,这是现在唯一能欺骗我还年轻着的借口了吧。rnrn所以,我十天半个月,就会想着离职,找工作,我太不专心了,但我觉得不只是我的问题。rnrn最近在看Joel在写的一本叫《软件随想录》的东西,虽然我觉得我在他面前充其量就一个SB吧,哈哈,但我还是觉得他说的一些东西很对。程序员应该学会写作,还有微观经济学,这才能让别人看到自己,而不是写着一堆优秀代码,然后让它们死在github或者sourceforge上。rnrn所以我考虑维护一下自己的博客:http://blog.csdn.net/gyj0754rnrn近期考虑把linux内核、linux驱动编程、android源代码分析和android编程的一些读书笔记共享一下。虽然这些内容或许网络上已经有人整理了,但出于训练、记忆强化和语言功底的磨练吧。写出来到底还是希望有人点评,毕竟只是一些未投入生产化的知识。rnrnrn不欢迎广告,讨论int,骂人,还有一开口就说不可能的人^_^ 论坛

没有更多推荐了,返回首页