◆传值与传引
严格来说,Java中所有方法参数都是传值。因为即使是传递的参数是对象的引数时,引数本身也是传值的。所有基本类型都是传值的。
传值:被传入的参数被视为内部的局域变量,其变化不影响外部变量的原始值。
传引:对于引数本身来讲也是传值的,但是在方法内部若对引数所指向的对象属性有改变,则会直接实时地影响所指向的对象属性。
理解传引这一概念的最佳方式就是画出指向图。eg. Aà(Object)O 对于A本身而言它是传值的,也就是说当A作为参数传递的时候,假若在方法内部把其它的引数赋给了A,但是当方法返回时,A仍旧是指向原来的对象,而不会改变。其次,对于传引来说,假若在方法内部对A所指向的对象属性有改变,那么对象属性的改变会是直接实时的。
再次强调,Java中所有的参数传递都是传值的。
传值这种题型的考试很多,基本类型传值问题考的较多的是对某一变量,故意用某一方法试图改变它,然后方法返回时再打印它。按传值的说法,实际上该变量并没有改变。
◆构造函数
a.构造器没有任何返回类型,哪怕是void也不行。假如指定了返回值,那么Java会视为一个普通的方法。
b.如果没有显示地调用父类的构造器,Java总是自动地调用父类的默认构造器。(也就是第一行会默认为super( ))
c.只要类中显式地定义一个或多个构造器,那么Java不会再为你定义一个默认的构造器
d.构造函数可以被重载,并且在其体内可以用this()和super()来调用其它的构造器。但是this()和super()只能放在构造函数体的第一行进行调用。
e.构造器的修饰符只可以是接入控制符public、private、protected、(default)。其它修饰符一律不可。
f.构造方法不可被继承。
◆重载与覆盖
重载的英文为Overload,而覆盖的英文为Override。重载是出现在同一类中多个同名函数,而覆盖是出现在类继承体系中同名函数。(注意:覆盖有时被称为重写)
重载是依据同名方法参数的个数、参数的类型和参数的顺序来确定的。方法的修饰符、返回值、抛出的异常均不能作为区分重载方法的依据。(继承体系中也有重载现象)
覆盖是在继承体系中子类覆盖超类中定义的方法。子类中覆盖方法的返回类型和参数类型必须精确匹配。接入控制符只能更加公有化;抛出的异常是超类抛出的子集或不抛。
(static方法不能覆盖,private方法也不能覆盖。Java视它们为被隐藏)
· 参数类型一致,返回类型不同,编译错误,提示“试图用不兼容的返回类型覆盖”。
· 只要参数类型不一致,返回类型同与不同,编译都能通过,此不为覆盖而是重载。
◆多态
多态是出现于类的继承层次中,通过向上转型和方法重写的机制来实现的。
面向对象程序设计的目标是:希望所撰写的程序代码都只对基类进行操作。这么一来,当加入新的继承类时,大部分程序代码都不会受影响而改写(也即是说代码具有扩充性)。所以当调用新加入的继承类时,都会首先向上转型为基类。这就是多态的向上转型。
当你希望通过调用基类的方法而能让继承类产生正确的行为时,这显然需要在继承类进行重写该方法。而究竟是该调用哪个继承类,这是由Java的动态绑定决定的。
多态最重要的一点在于run-time binding。多态是面向对象程序设计的目标。
关于多态中覆盖注意如下:
属性既可以继承,也是可以“覆盖”的。但是对属性而言没有动态绑定这一特性,所以覆盖的属性被认为是子类的特别属性。从某种意义上来讲,属性决定了类(性质)。另一方面,申明的类型就决定了对象的属性。这是因为,任何对象或变量等号右面是用来赋值给符合等号左面所申明类型的,所以左面的类型是先决条件,赋值必须要符合申明类型。对于向上转型而言,因为等号右面的对象is a申明类型,所以是成立的。一定要记住,属性永远都是跟着申明类型走。但是,对方法而言是在运行时动态绑定的,它取决于对象自身的实际类型(实际上对方法而言,也是先检查向上转型后的基类该方法,若无该方法的定义,则编译错,然后再动态绑定到继承类的覆盖方法)。
另外,static方法不能覆盖,private方法也不能覆盖。
还需特别注意,方法覆盖时,若覆盖得不对(例如参数一致,仅依靠返回类型不同),则编译会出错,而不是被Java认为方法重载(除非参数类型也不一样,这样java会认为不是override,实际上它是overload)。
◆类初始化
类的初始化大致上有这么几个方面。
a.静态变量的初始化 b.一般实例变量的初始化 c.构造函数的初始化
初始化的难点在于继承体系中。当有继承体系时,初始化始终要遵循的原则就是,无论如何必先初始化基类
0.当载入当前类时,若当前类有继承体系,则依次无条件载入基类
0’.先从最顶的基类开始顺序初始化其静态static变量,接着初始化当前类的静态static变量(也就是说,static变量的初始化是伴随着类被装载时而初始化的,它的初始化在任何构造函数执行前)
1.先从最顶端基类,构造基类对象。
(假如构造函数中有this或super调用,则先执行此调用)
1.1.首先按出现次序初始化其实例变量
1.2.再执行其构造函数体
2.依次递归上述步骤
<此外,实例变量可以不显式初始化(系统会赋默认值),但是局部变量必须显式初始化>
◆异常
throws是异常的申明,它置于方法的定义处;throw是异常的掷出,它置于方法体内。
异常可分为可检测异常和非检测异常,调用申明为可检测异常的方法时必须捕获异常。
a.方法申明了可检测异常,则调用该方法的块一定要捕获异常,否则编译出错
b.throw后面不能跟任何语句,否则编译提示语句不可到达
c.多个catch语句,要求更具体的异常在前,超类异常在后,否则编译出错
d.finally语句会在return语句之前执行,即在跳出方法之前一定会执行finally语句
e.假如遇到的是System.exit(0),则无论如何,程序马上退出,finally语句不会执行
f.方法申明了异常,但是在方法体内可以不显示地用throw抛出异常
g.没有申明可检测异常的方法调用时,不可用catch捕获,否则编译出错
其它注意:
a子类中覆盖的方法只能抛出父类方法抛出异常的子集,也可以不抛出任何异常(这本身就是子集)
b 但是对于非检测异常RuntimeException则不会受到上面的约束,它们可以被随时抛出。也不受范围限制。
c 当继承的子类没有申明异常时,假如它的一个实例被申明为超类(向上转型),这时再调用子类没有申明异常的方法,而用了catch,程序也会编译通过。(实际运行中调用的还是子类中的方法)
◆equals()和==
对于上述关于equals()总结如下:
a.类型不兼容的两个对象可以用equals(),但是只要比较的对象类型不同(哪怕值相同),永远返回false
b.对于没有覆盖equals()的类,即使对象类型相同,值也相同,但返回的仍旧是false,因为它用的是object的默认equals方法(与==相同)
c然而对于覆盖equals()的类,只要值相同,便返回true。这些类是String,Wrappers,Date,Calendar,BitSet等
对于==总结如下:
a.类型不兼容的两个对象不可以用==,若用则编译错误
b.同种类型的对象,假如不是指引同一个对象,则返回为false(只有指向同一个内存块的对象才返回true)
c.对于String情况有些不同,因为String对象有字面量和构造函数之分。字面量对象是放在缓冲池中的,这意味着,如果两个字面量值相同,则第二个对象会指向第一个已经存在的对象,而不会新产生,所以==返回的是true。用构造器产生的对象同一般对象。对于字面量来说,多个类共享的是同一个缓冲池。这意味着在另外一个类中创建一个先前类中已有的字面量字符串,则仍旧是同一对象。
另外,注意,toUpperCase()、toLowerCase()方法而言,如果大小写形式与原始String没什么差别,方法返回原始对象,==返回true。
d.对于基本类型而言,系统会自动先归一精度,然后再比较值,若值相同则返回true。
◆String
String类最重要的一点在于“不变性(immutable)”。它的意思是一旦String类产生了就不会再改变了,若试图改变它都会产生新的对象。
String对象有字面量和构造函数之分。字面量对象是放在缓冲池中的,这意味着,如果两个字面量值相同,则第二个对象会指向第一个已经存在的对象,而不会新产生,所以==返回的是true。用构造器产生的对象同一般对象。对于字面量来说,多个类共享的是同一个缓冲池。这意味着即使在另外一个类中创建一个先前类中已有的字面量字符串,则仍旧是同一对象。
考试中需要注意的是:s=new String(“abc”);s+=”def”;System.out.println(s);
s=new String(“abc”);s.concat(“def”);System.out.prinln(s);
前一程序打印为“abcdef”,后一程序打印为“abc”。区别是第一个程序又重新赋值给了s。而第二个程序中s.concat(“def”)只是产生了一个新的对象但没有赋给谁,但原来的s不变。
另外,对于StringBuffer而言是可变的,对它的任何改变都是实时的。
◆包装类
包装类是为了方便对基本数据类型操纵而出现的。有了包装类就可以用很多的方法来操纵基本数据类型(没有包装类想直接对基本数据类型操作是不方便的,除非自己编写方法)。
要熟悉包装类应该着种理解下面几点:
a.包装类的构造器。一般说来,包装类的构造器参数有两种:基本数据值、字符串
注意:Boolean包装类构造器当传入字符串时,除了不分大小写的true返回true外,其它字符串一律返回false
b.常见的操作方法。例如:转换为本基本类型或其它基本类型的方法
eg. byteValue(),intValue()…;parseByte(String s),parseInt(String s)…
c.valueOf(各基本类型或字符串)的使用。ValueOf(str)是包装类的静态方法,作用等同于构造器。它会解析传入的参数,然后构造一个相同类型的包装类,并返回该包装类。
例子:原始类型à字符串 (new Integer(101)).toString();String.valueOf(“101”)
字符串à(包装类)à原始类型 Integer.parseInt(“string”);
(new Integer(“101”)).doubleValue();Integer.valueOf(“101”).intValue()
◆Math类
Math类中都是静态方法。其中最易错的是三个方法:ceil(),floor(),round()
另外还需注意,有许多方法随基本数据类型不同有多个重载版本。eg.abs(),max()
a.ceil()方法。该方法返回类型为double,往单一的正坐标方向取最近的整数
b.floor()方法。该方法返回类型类double,取最靠近其负向的整数。
c.round()方法。它有两个重载版本:double和float,返回分别为long和int
long round(double a);int round(float)
round()方法=Math.floor(a+0.5),这意味着正数5入,负数6入
eg.System.out.println(Math.ceil(Double.MIN_VALUE)) //1.0
System.out.println(Math.floor(-0.1)) //-1.0
System.out.println(Math.round(-9.5)) //-9
System.out.println(Math.round(-9.6)) //-10
System.out.println(Math.round(Double.MIN_VALUE)) //0
◆collection类
collection类提供了持有对象的便利性,并对持有对象的操作便利性。正如其名,收集意为将各种类型的对象收在一起,且数目不限(有点像收集袋)。收集会将放入其中的所有对象均视为Object(向上转型),所以在取出元素对象时,必须显式(即强制转型)指出其类型。
对象收集从整体上分为Collection接口和Map接口。这种分类的标准是:某个元素位置上放置元素对象的个数。显然,Map接口放置的是一对。
Collection接口又可扩展为两个基本接口:List接口和Set接口。
依上所述,对象收集可以划分为四个基本的类型:Collection、List、Set、Map
· Collection 它是一个基类的接口,对元素没有任何的限制,可以重复并且无序。
· List 从其名就知是有序的列表。它描述的是按顺序将对象放入其中。显然,后放入的元素有可能与先前放入的对象是相同的。所以,List是允许对象重复出现的有序列表。
· Set 其实就是数学上所说的集合,它不允许有重复的元素。其中可以有空集(即null对象)。Set中的元素不要求有序。
· Map 即映射,借助于key和value来描述对象的搜索。key域中要求唯一性(其实就是一个Set),value域可以允许有重复的元素(其实就是一个Collection)。另外注意:常见的HashMap是无序的,而TreeMap是有序的。
◆标识符
a.所有标识符的首字符必须是字母(大小写)、下划线_、美元符号$(或符号¥)
b.接下来的可以是由数字(0-9)及首字符相同类型的字符(字母、_、$),其它任何特殊字符均不可
c.标识符不能使用Java关键字和保留字(50个)。但是注意像java,Integer,sizeof,friendly等都不是Java的关键字
d.标识符大小写是敏感的,但没有长度的限制。
◆Switch(i)
a.switch(i)中的参数最高精度是int(或者short,byte,char),但不可是long,float,double
b.default语句可以放置于任何地方(default意为都不匹配case中的值)
c.当语句中未加break语句时,则程序会从匹配的地方开始执行(包括匹配default语句),接下来所有的语句都会被执行(而不管匹配否),直到遇到break语句或switch尾部。
◆垃圾收集
a.只要一个对象失去了所有的reference,就可以考虑收集到垃圾收集堆了。
B.当失去对一个对象的所有引用时,JVM只是考虑垃圾收集,但并不意味着就立刻收回这个对象的内存,甚至根本不收回。JVM仅会在需要更多的内存以继续执行程序时,才会进行垃圾收集。
C.多数情况下,你永远不会知道垃圾收集什么时候会发生。Java将垃圾收集进程作为一个低优先级线程在运行。在Java中垃圾收集是不能被强迫立即执行的。调用System.gc()或Runtime.gc()静态方法不能保证垃圾收集器的立即执行,因为,也许存在着更高优先级的线程。
D.如果你想人工调用垃圾收集,并想在收集对象时执行一些你想要的任务,你就可以覆盖一个叫finalize()的方法。java会为每一个对象只调用一次finalize()方法。finalize()方法必须被申明为protected的,不返回任何值(viod),而且要申明抛出一个Throwable对象,并一定要调用超类的finalize()方法(super.finalize())。
eg.protected void finalize() throws Throwable(){
super.finalize();
…………;}
◆is a & has a
is a 描述的是一个超类和一个子类的关系,也即是继承的关系。
has a 描述的是一个对象的部分是另一个对象,也即是组合的关系(或称为调用)。
◆内类与匿名类
内类是被包含的类中类,有三个方面需要注意:一般内类、方法内类、静态内类。
· 一般内类它可以被当做外类的一个“实例变量”来看待。因此,四个接入控制符public、protected、default、private。只是注意:要在外类的non-static函数外产生该内类对象,必须以OuterClassName.InnerClassName的形式指定该内类对象的类型申明。一般内类必须得关联至其外类的某个对象。
一般内类不可拥有static成员。
· 方法内类它属于范围型内类,也就是说你无法在方法外来调用内类,从这一点来讲它可视为方法的局部变量。但是,虽然有它的范畴性,毕竟内类还是类,它不会像局部变量那样随着方法的返回就消失了,它仍旧被java视为类。
A.方法内类可以直接访问外类的任何成员
B.方法内类只能访问该方法中final型局部变量和final型的方法参数
C. 方法内类不可有任何接入控制符修饰(这一点与局部变量相同)
· 静态内类它在产生其对象时不需要存在一个外类对象。它可被视为static函数。
static内类可以置于接口中。
· 匿 名 类它实际上是继承自new类的一个无名类。New传回的reference会被自动向上转型。匿名类不能拥有构造器,但是可以通过其基类默认或带参数的构造器来申明。
匿名类添加任何修饰符(遵循超类的修饰符),也不可实现接口、抛出异常。
◆断言
断言是Java 1.4中新添加的功能,是Java中的一种新的错误检查机制,它提供了一种在代码中进行正确性检查的机制,但是这项功能可以根据需要关闭。断言包括:assert关键字,AssertionError类,以及在java.lang.ClassLoader中增加了几个新的有关assert方法。
assert最重要的特点是assert语句可以在运行时任意的开启或关闭,默认情况下是关闭的。
断言语句有两种合法的形式:a.assert expression_r1; b.assert expression_r1 : expression_r2;
expression_r1是一条被判断的布尔表达式,必须保证在程序执行过程中它的值一定是真;expression_r2是可选的,用于在expression_r1为假时,传递给抛出的异常AssertionError的构造器,因此expression_r2的类型必须是合法的AssertionError构造器的参数类型。AssertionError除了一个不带参数的缺省构造器外,还有7个带单个参数的构造器,分别为:object(eg.String) boolean char int long float double。第一种形式如果抛出异常,则调用AssertionError的缺省构造器,对于第二种形式,则根据expression_r2值的类型,分别调用7种单参数构造器中的一种。
A.assert程序的编译:javac -source 1.4 TestAssert.java(提示java按1.4版本编译)
B.assert程序的运行:java –ea TestAssert 或者 java –ea:TestAssert TestAssert
其它的运行参数:java -ea:pkg0... TestAssert;java –esa;java –dsa(系统类断言),另外,还可以同时组合用。当一个命令行使用多项 -ea -da 参数时,遵循两个基本的原则:后面的参数设定会覆盖前面参数的设定,特定具体的参数设定会覆盖一般的参数设定。
C.AssertinError类是Error的直接子类,因此代表程序出现了严重的错误,这种异常通常是不需要程序员使用catch语句捕捉的。
D.使用assert的准则:assert语句的作用是保证程序内部的一致性,而不是用户与程序之间的一致性,所以不应用在保证命令行参数的正确性。可以用来保证传递给private方法参数的正确性。因为私有方法只是在类的内部被调用,因而是程序员可以控制的,我们可以预期它的状态是正确和一致的。公有方法则不适用。此外,assert语句可用于检查任何方法结束时状态的正确性,及在方法的开始检查相关的初始状态等等。
assert语句并不构成程序正常运行逻辑的一部分,时刻记住在运行时它们可能不会被执行。
◆线程
线程是将程序中容易消耗大量cpu且易陷入死循环的片断代码独立出来作为一个线程来运行(也即线程是一个代码块)。
线程一经启动start,就会进入ready状态(实际上就是runnable状态,只是等待分配cpu)。这也说明线程并不会马上就running,其运行具有不确定性。线程启动后,只要不跳出run()方法,则一直都有机会running,它由系统自动在各线程间分配cpu时间来running。要牢记的是:线程运行与中断具有不确定性,你永远也不知道线程何时会运行,何时会中断。
线程从对象的角度来看,它自身也可以是一个对象。它可视为其它对象中的一个代码块。
在线程的概念中要特别注意几个概念:单线程、多线程、多线程的运行、多线程间的同步(资源访问)、多线程间的通信。
· 单线程 对于单线程而言,编写其程序是比较简单的,也比较容易理解,因为它并不涉及到synchronized和communication问题。创建单线程的方法有两种,其一、扩展Thread类,即class A extends Thread{public void run(){};……};其二、实现Runnable接口,class B implements Runnable{Thread t=new Thread(this);public void run(){};……};
· 多线程 相对于单线程而言,多线程会复杂很多,原因就是它们会涉及到多线程间的资源访问和多线程间通信问题。这就涉及到下面所说的三个方面:多线程的运行、多线程间的同步(资源访问)、多线程间的通信
· 多线程的运行 对于多个可运行runnable的线程来说,运行与中断具有不确定性,永远也无法知道线程何时会运行,何时会中断。但是多线程运行也遵循几个原则:如果多个线程具有同样的优先级,则系统会在它们之间切换cpu时间运行;JVM基于优先级来决定线程的运行,但是这并不意味着一个低优先级的线程一直不运行。
· 多线程的同步 被线程可访问的每个对象都有一个仅被一个线程访问控制的锁,锁控制着对对象的同步码的存取。这个可被多个线程访问的对象就是所说的资源共享问题。
A.在程序中,可以通过定义一个synchronized代码块或多个synchronized方法来使得调用的线程获得该对象的控制锁。通过获得对象的锁,该线程就会阻止其它线程对该对象所定义的同步块或同步方法进行操作(特别注意的是,此时并不能保证其它线程对该对象的非同步变量和非同步方法进行操作)。
B.线程只有在同步块或同步方法返回后才释放锁。
C.synchronized并不能保证程序的运行连续性,而只是保证同一性。也就是说即使在synchronized块或方法中,线程的运行仍旧会有中断的可能性。尽管如此,但它却能确保别的线程不会再访问该对象的其它同步块和方法,因为对象锁并未释放。这一事实说明了“多线程的运行”与“多线程间的同步”是两个独立的概念。
D.多线程的同步块或方法可以放在任何可被线程访问的对象中(包括线程本身,它实际上也可被其它的线程访问)。
· 多线程间的通信 多线程间的同步消除了一个线程在改变另一个线程的稳定对象状态时发生的并发错误,但是就线程间通信而言,同步不起任何作用,也就是说“多线程间的通信”又是一个独立的概念。多线程间的通信通常是靠wait(),notify()两个方法来实现的,有关这两个方法的总结如下:
1.wait(),notify()属于object方法,并不是线程的方法
2.object.wait() 意为:调用我(指该object)的当前线程你得等等,也就是使...(调用我的当前线程)...等待
object.notify()意为:该唤醒其它先前调用过我的且在等待的线程
从上述意义可知,wait(),notify()的对象是指线程所要用到的共享对象(当然共享对象也可以是线程对象),但是它的方法动作却是针对调用它的线程。(通常情况下,对象的方法是作用于自己的属性,而很少作用于其它对象。若要作用于其它对象,则用调用object.method())
3.wait(),notify()必须成对出现,出现的方式可有3种形式。
A.{wait();.... notify();}
B.{wait();}... {object.notify();}
C.{notify();}... {object.wait();}
4.wait(),notify()必须出现在synchronized方法或块中,否则会出现异常。原因是因为wait()会释放对象锁,而锁必然是出现在同步方法或块中。另外,wait()同sleep()一样,也必须捕捉异常InterruptedException。
5.wait(),notify()的执行一般与外在的条件有关,只有条件改变了才触发唤醒等待的线程。这种条件的改变通常是以旗标(Tag)的方式出现,也即当某一方法执行完后,应当立即改变旗标值。假若需要让线程交替执行,则还需要加入互斥条件的判断。eg.同步方法1中{if(a)},则同步方法2中{if(!a)}
6.当执行完notify()时,程序并不会立即去运行调用wait()的线程,而直到释放notify()的对象锁。当释放完锁后,程序重新分配cpu,要注意的是,此时系统并不一定就让wait的线程去运行,而有可能是刚才调用notify()的线程接着继续运行。这一点正说明了线程运行与中断的不确定性。
7.一般说来,notify()是唤醒等待池中等待时间最长的线程;而notifyAll()是唤醒等待池中所有等待线程,然后线程去竞争对象锁。这里说的是一般情况,有时情况并非如此,这是由系统中线程运行与中断的不确定性决定的。
8.wait(),notify()通常情况下需要sleep()的配合,否则屏幕中的运行显式会“飞速”。
9.多线程间的通信会出现死锁现象,即wait的线程有可能永远也得不到对象锁。
-------------------------------------------------------------------------------
◆其他注意问题
☆ 数组
a.数组在使用之前,必须要保证给其分配内存(系统会用默认值初始化),不可只定义。否则编译通过运行也会出现空指针错误。分配内存只需通过new就可以了。
b.二维数组的第二维可以是变长的,而且可以在定义时不指定具体值。这意味着java中的二维数组不必像矩阵那样要求每一维长度都相同。
☆ 变量赋值
a.实例变量只可在定义时显式赋值,不可先定义,再赋值(这样的话编译出错)。
b.方法变量既可以在定义时显式赋值,又可以先定义以后再赋值。
c.static变量可以在类的任何地方赋值(若在方法中赋值,实际上是重赋值了)。
d.final变量可以在任何地方赋值,但是一旦赋值,就不允许再次重赋值。
e.static final变量只能在定义处赋值(即:常量)。
☆ 移位
a.>> 首位的拷贝右移位。等同于有符号的除法。
b.>>> 零填充右移位。
c.<< 左移位。等同于有符号乘法,但是必须遵循整数的溢出原则。
d.>>32 >>>32任何移多少位都是移模32的余数。eg.移32位即不移。
☆ byte、char和int
由于char的取值范围和int的正取值范围相同,所以,整型字面量可以直接赋给char。但是要是明确将一个整型(int)变量直接赋给char变量则编译错误。
另外,int i=5;char c=’a’;c+=i;编译是通过的。
byte类型在强制转型的情况下,当范围超出时会循环溢出。
☆ 求模%
求余只管左边的符号,右边不管。
eg. int a=-5;int b=-2;System.out.println(a%b) //-1
int a=-5;int b=2;System.out.println(a%b) //-1
int a=5;int b=-2;System.out.println(a%b) //1