本文目录一览:
问一个JAVA的小问题
这个教程向您介绍java的泛型(generic)。你可能熟悉其他语言的泛型,最著名的是C++的模板(templates)。 如果这样,你很快就会看到两者的相似之处和重要差异。如果你不熟悉相似的语法结构,那么更好,你可以从头开始而不需要忘记误解。
Generics允许对类型进行抽象(abstract over types)。最常见的例子是集合类型(Container types),Collection的类树中任意一个即是。
下面是那种典型用法:
List myIntList = new LinkedList();// 1
myIntList.add(new Integer(0));// 2
Integer x = (Integer) myIntList.iterator().next();// 3
第3行的类型转换有些烦人。通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。但是,这个类型转换是必须
的(essential)。编译器只能保证iterator返回的是Object类型。为了保证对Integer类型变量赋值的类型安全,必须进行类型转换。
当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(run time error),因为程序员可能会犯错。 程序员如何才能明确表示他们的意图,把一个list中的内容限制为一个特定的数据类型呢?这是generics背后的核心思想。 这是上面程序片断的一个泛型版本:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = myIntList.iterator().next(); // 3
注意变量myIntList的类型声明。它指定这不是一个任意的List,而是一个Integer的List,写作:List。
我们说List是一个带一个类型参数的泛型接口(a generic interface that takes a type parameter),本例中,类型参数是Integer。我们在创建这个List对象的时候也指定了一个类型参数。
另一个需要注意的是第3行没了类型转换。现在,你可能认为我们已经成功地去掉了程序里的混乱。我们用第1行的类型参数取代了第3行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说myIntList被声明为List类型,这告诉我们无论何时何地使用myIntList变量,编译器保证其中的元素的正确的类型。与之相反,一个类型转换说明程序员认为在那个代码点上它应该是那种类型。实际结果是,这可以增加可读性和稳定性 (robustness),尤其在大型的程序中。
2. 定义简单的泛型
下面是从java.util包中的List接口和Iterator接口的定义中摘录的片断:
public interface List {
void add(E x);
Iterator iterator();
}
public interface Iterator {
E next();
boolean hasNext();
}
这些都应该是很熟悉的,除了尖括号中的部分,那是接口List和Iterator中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces List and Iterator)。类型参数在整个类的声明中可用,几乎是所有可以使用其他普通类型的地方(但是有些重要的限制,请参考第7部分)
在介绍那一节我们看到了对泛型类型声明List(the generic type declaration List)的调用,如List。
在这个调用中(通常称作一个参数化类型a parameterized type),所有出现形式类型参数(formal type parameter,这里是E)的地方
都被替换成实体类型参数(actual type argument)(这里是Integer)。 你可能想象,List代表一个E被全部替换成Integer的版本:
public interface IntegerList {
void add(Integer x)
Iterator iterator();
}
这种直觉可能有帮助,但是也可能导致误解。 它有帮助,因为List的声明确实有类似这种替换的方法。 它可能导致误解,因为泛型声明绝不会实际的被这样替换。没有代码的多个拷贝,源码中没有、二进制代码中也没有; 磁盘中没有,内存中也没有。如果你是一个C++程序员,你会理解这是和C++模板的很大的区别。
一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。类型参数就象在方法或构造函数中普通的参数一样。就像一个方法有形式参数(formal value parameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formal type parameters)。当一个方法被调用,实参(actual arguments) 替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。
一个命名的习惯:我们推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母, 这使它和其他的普通的形式参数很容易被区分开来。许多容器类型使用E作为其中元素的类型,就像上面举的例子。 在后面的例子中还会有一些其他的命名习惯。
3. 泛型和子类继承
让我们测试一下我们对泛型的理解。下面的代码片断合法么?
List ls = new ArrayList(); //1
List lo = ls; //2
第1行当然合法,但是这个问题的狡猾之处在于第2行。 这产生一个问题: 一个String的List是一个Object的List么?大多数人的直觉是回答:“当然!”。 好,在看下面的几行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 试图把Object赋值给String
这里,我们使用lo指向ls。我们通过lo来访问ls,一个String的list。我们可以插入任意对象进去。结果是ls中保存的不再是String。当我们试图从中取出元素的时候,会得到意外的结果。 java编译器当然会阻止这种情况的发生。第2行会导致一个编译错误。
总之,如果Foo是Bar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G是G的子类型并不成立!!
这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。这种直觉的问题在于它假定这个集合不改变。我们的直觉认为
这些东西都不可改变。 举例来说,如果一个交通部(DMV)提供一个驾驶员表给人口普查局,这似乎很合理。我们想,一个List 是一个List,假定Driver是Person的子类型。实际上,我们传递的是一个驾驶员注册的拷贝。然而,
人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。为了处理这种情况,考虑一些更灵活的泛型类型很有用。
到现在为止我们看到的规则限制比较大。
4.通配符(Wildcards)
考虑写一个例程来打印一个集合(Collection)中的所有元素。下面是在老的语言中你可能写的代码:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k c.size(); k++) {
System.outi.next());
}
}
下面是一个使用泛型的幼稚的尝试(使用了新的循环语法):
void printCollection(Collection c) {
for (Object e : c) {
System.out.println(e);
}
}
问题是新版本的用处比老版本小多了。老版本的代码可以使用任何类型的collection作为参数,而新版本则只能使用 Collection,我们刚才阐述了,它不是所有类型的collections的父类。那么什么是各种collections的父类呢?它写作:
Collection(发音为:"collection of unknown")
就是,一个集合, 它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:
void printCollection(Collection c) {
for (Object e : c) {
System.out.println(e);
}
}
现在,我们可以使用任何类型的collection来调用它。注意,我们仍然可以读取c中的元素,其类型是Object。这永远是安全的,
因为不管collection的真实类型是什么,它包含的都是objects。但是将任意元素加入到其中不是类型安全的:
Collection c = new ArrayList();
c.add(new Object()); // 编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。 add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型, 所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。 另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object,因此把get的返回值赋值给一个Object类型的对象或者放在任何希望是Object类型的地方是安全的。
4.1. 有限制的通配符(Bounded Wildcards)
考虑一个简单的画图程序,它可以用来画各种形状,比如矩形和圆形。 为了在程序中表示这些形状,你可以定义下面的类继承结构:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) { // ... }
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) { // ... }
}
这些类可以在一个画布(Canvas)上被画出来:
publicclass Canvas {
publicvoid draw(Shape s) {
s.draw(this);
}
}
所有的图形通常都有很多个形状。假定它们用一个list来表示,Canvas里有一个方法来画出所有的形状会比较方便:
public void drawAll(List shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
现在,类型规则导致drawAll()只能使用Shape的list来调用。它不能,比如说对List来调用。这很不幸, 因为这个方法所作的只是从这个list读取shape,因此它应该也能对List调用。我们真正要的是这个方法能够接受一个任意种类的shape: 软件开发网
public void drawAll(List shapes) { //..}
这里有一处很小但是很重要的不同:我们把类型 List 替换成了List。现在drawAll()可以接受
任何Shape的子类的List,所以我们可以对List进行调用。
List是有限制通配符的一个例子。这里?代表一个未知的类型,就像我们前面看到的通配符一样。
但是,在这里,我们知道这个未知的类型实际上是Shape的一个子类(它可以是Shape本身或者Shape的子类)。我们说Shape是这个通配符的上限(upper bound)。 像平常一样,要得到使用通配符的灵活性有些代价。这个代价是,现在向shapes中写入是非法的。比如下面的代码是不允许的:
public void addRectangle(List shapes) {
shapes.add(0, new Rectangle()); // compile-time error!
}
你应该能够指出为什么上面的代码是不允许的。因为shapes.add的第二个参数类型是? extends Shape——一个Shape未知的子类。 因此我们不知道这个类型是什么,我们不知道它是不是Rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个
Rectangle不安全。 有限制的通配符正是我们解决DMV给人口普查局传送名单的例子所需要的。我们的例子假定数据用一个姓名(String) 到people(用Person或其子类来表示,比如Driver)的映射表示。Map是一个有两个类型参数的泛型类型的例子,表示map的键key和值value。 再一次,注意形式类型参数的命名习惯——K代表keys,V代表vlaues。
public class Census {
public static void addRegistry(Map registry) { ...}
}
Map allDrivers = ...;
Census.addRegistry(allDrivers);
5. 泛型方法
考虑写一个方法,它用一个Object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。
下面是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection c) {
for(Object o : a) {
c.add(o); // 编译期错误
}
}
现在,你应该能够学会避免初学者试图使用Collection作为集合参数类型的错误了。或许你已经意识到使用
Collection也不能工作,你不能把对象放进一个未知类型的集合中去。 解决这个问题的办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
static void fromArrayToCollection(T[] a, Collection c){
for (T o : a) {
c.add(o); // correct
}
}
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
Object[] oa = new Object[100];
Collection co = new ArrayList();
fromArrayToCollection(oa, co);// T 指Object
String[] sa = new String[100];
Collection cs = new ArrayList();
fromArrayToCollection(sa, cs);// T inferred to be String
fromArrayToCollection(sa, co);// T inferred to be Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection cn = new ArrayList();
fromArrayToCollection(ia, cn);// T inferred to be Number
fromArrayToCollection(fa, cn);// T inferred to be Number
fromArrayToCollection(na, cn);// T inferred to be Number
fromArrayToCollection(na, co);// T inferred to be Object
fromArrayToCollection(na, cs);// compile-time error
注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型参数的值。
它通常推断出能使调用类型正确的最明确的类型参数(原文是:It will generally infer the most specific type argument that will make the call type-correct.)。
现在有一个问题:我们应该什么时候使用泛型方法,又什么时候使用通配符类型呢? 为了理解答案,让我们先看看Collection库中的几个方法。
public interface Collection {
boolean containsAll(Collection c);
boolean addAll(Collection c);
}
我们也可以使用泛型方法来代替:
public interface Collection {
boolean containsAll(Collection c);
boolean addAll(Collection c);
// hey, type variables can have bounds too!
}
但是,在 containsAll 和 addAll中,类型参数T 都只使用一次。返回值的类型既不依赖于类型参数(type parameter)也
不依赖于方法的其他参数(这里,只有简单的一个参数)。这告诉我们类型参数(type argument)被用作多态(polymorphism),
它唯一的效果是允许在不同的调用点,可以使用多种实参类型(actual argument)。如果是这种情况,应该使用通配符。 通配符就是被设计用来支持灵活的子类化的,这是我们在这里要强调的。
泛型函数允许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。如果没有这样的依赖 关系,不应该使用泛型方法。
(原文:Generic methods allow type parameters to be used to express dependencies among the types of one
or more arguments to a method and/or its return type. If there isn’t such a dependency, a generic method should not be used.)
一前一后的同时使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():
classCollections {
public static void copy(List dest, List src){...}
}
注意两个参数的类型的依赖关系。任何被从源list从拷贝出来的对象必须能够将其指定为目标list(dest) 的元素的类型——T类型。因此源类型的元素类型可以是T的任意子类型,我们不关心具体的类型。 copy方法的签名使用一个类型参数表示了类型依赖,但是使用了一个通配符作为第二个参数的元素类型。我们也可以用其他方式写这个函数的签名而根本不使用通配符:
class Collections {
public static void copy(List dest, List src){...}
}
这也可以,但是第一个类型参数在dst的类型和第二个参数的类型参数S的上限这两个地方都有使用,而S本身只使用一次,
在src的类型中——没有其他的依赖于它。这意味着我们可以用通配符来代替S。使用通配符比声明显式的类型参数更加清晰
和准确,所以在可能的情况下使用通配符更好。 通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组。这就有一个例子。
回到我们的画图问题,假定我们想要保持画图请求的历史记录。我们可以把历史记录保存在Shape类的一个静态成员变量里,
在drawAll() 被调用的时候把传进来的参数保存进历史记录:
static ListList history = new ArrayListList();
public void drawAll(List shapes) {
history.addLast(shapes);
for(Shape s: shapes) {
s.draw(this);
}
}
最终,再说一下类型参数的命名习惯。 我们使用T 代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数, 我们可能使用字母表中T的临近的字母,比如S。如果一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。对内部类也是同样。
关于如何学好JAVA的文章
51CTO编者注:这篇文章已经是有数年“网龄”的老文,不过在今天看来仍然经典。如何学习Java?本篇文章可以说也是面对编程初学者的一篇指导文章,其中对于如何学习Java的步骤的介绍,很多也适用于开发领域其他技能的学习。
【在原先《学好Java之我见》的基础上重新整理而成】
Java - 近10年来计算机软件发展过程中的传奇,其在众多开发者心中的地位就如“屠龙刀”、“倚天剑”。
Java是个平台,我只想说说我对学好Java的一点看法,希望对初学者有所帮助。
1. 思考一下
学习Java之前,先别急,静下心来好好想想:
1)你对学习Java是否有兴趣?
2)你是否能静下心来坚持不懈地学习?
嗯,这是个容易但又绝对不应该忽略的问题,你确信自己对Java感兴趣、而且又有吃苦的准备,那你才可能学好Java!如果具备这两点条件,就请继续往下看……
2. 准备一下
请不要把你的学习Java之路和其它计算机技术分开看待,技术的联系往往是千丝万缕的,你应该掌握一些学习Java所涉及的基础知识,对于 “CLASSPATH要怎么设置啊”、“跪求JDK下载地址”等等问题,你不该问,因为Internet上太多答案了,甚至换个角度说,你是不是还不适合直接学习编程?
1)买本Java学习用书。
JDK 6正式版刚刚推出,市面上你暂时买不到JDK 6的书籍,但我仍推荐你买一本适合入门的、较新JDK版本的Java书籍,那么,你现在选择一本讲述JDK 5的入门书籍还是有条件的。我可以推荐一些入门书,但是我不打算这么做,因为合我口味的,不一定适合你。但无论如何,《Thing In Java》绝对不应该作为你入门Java的第一本书籍!
记住,网络上学习Java的资源很多,但基本不够系统,拥有一本Java入门书籍,是你系统学习Java的基础。
2)准备Java学习环境。
准备你的学习环境,很简单,安装JDK 6,然后用类似UltraEdit、EditPlus的文本编辑器配置你的学习环境,Internet上有很多关于如何配置的文章。初学Java,不建议用IDE工具,通过一行行的敲代码,你会碰到很多问题,只有这样,你才能学会怎样解决问题,并加深自己对Java的理解。
准备好后,开始进入激动人心的Java学习里程吧!
3. Java基础学习之路
学习Java的过程虽然辛苦些,但又是处处有精彩!学习过程中的心态一定要保持专一,网上关于语言间的“PK”到处都是,别被浮躁影响!认准了Java,你就坚持!克服心魔,恒心最终会给你回报的。
Java的体系分为Java SE、Java EE和Java ME(JDK 5.0以前分别叫J2SE、J2EE和J2ME),Java的敲门砖就是Java SE了,也就是你要入门学习的内容,不论你日后是选择哪个体系领域作为发展方向,你都得从Java SE起步。
学习Java SE,打好Java基础;如果想学Java EE(对不起,我不了解Java ME,所以我无法涉及Java ME的相关内容),对于AWT、Swing是否要学习呢,我个人是觉得还是要知道其所以然的,特别是其事件处理模式,我强烈建议初学者一定要弄清楚,其他具体的开发就不一定要全面掌握了,毕竟AWT、Swing方面的内容足够用几本大部头的书才能描述详尽;当然,如果你的工作就是做 AWT、Swing开发,那就另当别论了。
I/O、Thread都是基础之一。
4. Java EE学习之路
学习了Java SE,还只是完成“万里长征”的第一步。
接下来选择学习Java EE或是Java ME(或者你还想不断深入学习Java SE,只要你喜欢,你可以一直深入下去,就像前面所说的,我不会讲到Java ME的学习);估计选择Java EE方面的学习者会多一些,Java EE所涉及技术非常多,考虑到初学者的接受程度,我会尽量从最常用的角度来介绍。
学习Java EE,在开发环境上不建议再用文本编辑器,我感觉NetBeans 5.5(目前最高正式版本)很适合初学者,我个人也很喜欢NetBeans,如果你喜欢用别的IDE如Eclipse,都没问题,看自己喜欢吧。
4.1 学习JSP/Servlet
Java EE最高版本目前是5.0版本。
在Java EE中,首先要学习JSP/Servlet(结合Tomcat、MySQL)技术,建议JSP1.2/2.X的知识都要掌握,毕竟现在很多的在用系统都还是基于JSP1.2,学习JSP,还必须掌握一些外延技术,如:你还得掌握HTML基础知识,最好再掌握些JavaScript(目前正火的AJAX技术之一)、CSS的技术,而了解XML是必不可少的(至少描述性配置信息是通过XML来描述的)。在学习JSP/Servlet过程中,应该结合学习 JDBC、数据库开发技术,毕竟绝大多数Java Web项目都是和数据库紧密结合的。
4.2 学习数据库开发技术
这里我想重点说一下数据库开发技术,数据库技术是做业务系统必备技能,Java Web开发人员最低程度都应该掌握SQL语句的使用!数据库技术大体可分为DBA技术和数据库开发技术,对于开发而言,应侧重数据库开发技术,而数据库基本操作,也是要掌握的。常用数据库有DB2、Oracle、SQL Server、MySQL等,你可以结合实际需要,选择一种数据库,并花大力气掌握其开发技术。
记住,学习数据库开发技术和学习Java EE,是相对独立的,你可以在学习JSP/Servlet的同时,学习数据库开发技术(如SQL语句等),或者先学数据库开发技术,之后再学习JSP/Servlet。
4.3 学习Java EE其它开发技术
再往后,可以学习一些EJB的知识,目前EJB最新版本是3.0,其和前面的2.X版本相比,有较大不同,考虑到目前EJB 2.X还有很大的应用市场,建议结合学习EJB 2.X/3.0。在学习EJB过程中,应同时掌握一种企业级应用服务器的使用(如 WebSphere、Weblogic、Sun Applcation Server或JBOSS等,目前Sun Applcation Server 9.X和JBOSS都可支持EJB 3.0)。
之后的学习中,再逐渐扩展到其他Java EE技术。
5. 之后
等你对Java EE有些感悟的时候,你自然就会想不断扩展你的知识面,这时候你可以学习Spring、Hibernate,以及各种Web框架(如Struts、JSF、Webwork或是Tapestry等)。
随着技术、经验的不断积累,你会逐步关注分析、设计等更高层次的知识,这时候,你可进一步学习相关的UML、模式等知识(积累了一定经验,你就可以安排自己学习这些知识了)。
6. 小结
永远记住:自始至终,实践是学习Java技术历程中极其重要的一环。脱离了实践,是学不好技术的!
Java学习、实践之路依然没有银弹,况且一步步走来,要学的东西很多,勤奋是金!
善于利用Internet上众多的资源,多吸取些别人的经验。
目前关于Java的争论很多,如“Java正走下坡路”、“Ruby必将取代Java”等论点甚嚣尘上,但正如我前面所言,这是一种浮躁的表现,Java的前景非常好,特别是Java开源以后!就让他们去吵去吧,掌握了Java,你就掌握了当前最火的技术。
“梅花香自苦寒来”,当你掌握了扎实的Java开发基础,你就能把手中的这把“屠龙刀”、“倚天剑”舞得风声水起!那时,你一定会有一种成就感。
以上,你大概了解应当如何学习Java了么?
Java 中的一些小问题~
如果你了解它们在内存里的关系就会懂了
A里有个对象B
A a = new A()时,里面的b也new了,这时内存堆里有两个实例,内存栈里有两个引用分别指向堆里的两个实例,
如果你把a的引用指向了null,对于你的问题b是否会等于null,这有两种情况
一种是:实例a被一个以上的引用指向,a是其中一个,如果a指向了null,但是实例a还有其他引用指向,b不会等于null
第二种是:实例a只有一个引用指向,当你把这个引用指向null后,那么实例就没有引用指向它了,那么java的垃圾回收器就会把实例a这个销毁,当然里面的b也会销毁,但是,如果里面的实例b在外部还有其他的引用指向的话,那么b也还是会存在
这个要自己慢慢做实验来了解比较好
向你上面的程序,你把a指向null,那么你要怎么访问b?调用a.getB()不就出现NullPointerException了吗
但是你可以这样
A a = new A();
A a1 = a;
a = null;
a1.getB();你看b是不会为空的