取这个标题,还是感觉有些大言不惭。之前大三的时候自学过一些基本的java知识,大概到了能独立写一个GUI出来的水平把,不过后来随着有了其他目标,就把这块放下了。之后常年没有用,早就忘得精光。这次重拾Java,还是从最基本的看起。不过因为还保留着之前一点记忆以及在Python里获得的一些知识,可能写的是非常不完全的,很多东西我懂的话也就跳过了。
■第一话,命运之出会
绪论的绪论。。
Java是典型的OOP语言,即面向对象程序设计语言。编程语言一路发展而来,从机器语言,汇编语言走到了较为高级接近自然语言的现代编程语言。不过现代语言中也分成很多种,早期的语言解决问题时还是需要程序员把问题转化成计算机思考的模式,从来没有让他们真正地基于问题来解决问题。这主要是因为问题的模式多种多样,很少能够找到一种编程的组织方式来解决所有模式的问题。随着程序语言的发展,人们渐渐意识到了OOP可能是一个比较好的突破口。
在OOP的设计模式中,问题被更高度地抽象。OOP中有很多对象,每一个对象封装了一定的功能,可以视为是一个微型计算机,具有状态,还具有操作而用户可以要求它去执行这些操作。
■Java的变量、对象、类、方法
每个语言都有一些内设的基本类型。基本类型越多,就代表着语言的聚合度越高。在Java中,有以下几种基本类型供使用者选择
boolean类,char类,byte类,short类,int类,long类,float类,double类,void。其中,从short到double的所有数值类型都带有正负号,几种整数类型的范围是short在±2**15,int在±2**31,long在±2**63左右。boolean类的取值是true和false(注意和python不一样首字不大写)。
基本类型针对对象而言,一个有类型的对象才是可以被解析的。而变量是对对象的引用。变量有作用域的概念,Java中的作用域可以通过看大括号包括的范围看出来。父作用域中的定义的变量可以在子作用域中使用,但是反过来不行。另外特别特别需要注意的是在Java中是不允许子作用域的变量覆盖父作用域的变量,或者同作用域内存在相同名字的变量。比如下面这段代码:
pulicclasstest{publicstaticvoidmain(Stringargs[]){inti=0;intk=1;if(k==1){inti=2;//报错,会报出变量名冲突的错误}}}类似的逻辑在C或者python中是语法正确的,在if语句块的作用域结束之后,下面的变量i的值将会变成2。Java的设计者认为,重复的变量命名会引起混乱所以不允许这么做。
值得一提的是,虽然引用会跟着作用域失效而失效,但是引用指向的对象不会自动地销毁。在C/C++等较底层的程序语言中,如果不释放已经没有用的对象就会导致内存空间浪费,最终变成内存泄露的问题。而Java和Python一样有一套垃圾回收系统,监视着所有new出来的对象,一旦确认对象没有用了就会把这个对象回收释放。
类中会有一些操作,以方法的形式体现。方法定义时格式是ReturnTypemethodName(args1,args2...){/*methodbody*/}。然后调用时应该使用objectName.methodName(args1,args2...)。args是参数,Java中定义方法时需要把接受参数的类型写清楚,否则会报错。如果规定了方法的返回值是void的话,那么就可以只写return;,否则编译器会强制要求返回一个具体的值比如returntrue;或者return1.1;。
publicclasstest{publicstaticvoidmain(Stringargs[]){MyTestt1=newMyTest();MyTestt2=newMyTest();}}classMyTest{staticinti=47;}对于MyTest类的两个实例,t1和t2都可以引用i。但是它们对i的引用指向同一个对象。同时静态变量或方法也可以直接通过类名来引用,MyTest.i引用到的对象和t1.i以及t2.i都是一样的。当通过这三个引用中任意一个改变对象的值的话,剩余两个也会反映出这种变化。这个和Python中的类变量有着相似和不同的地方。
●Java中的static和python中的类变量
所谓Python中的类变量是这样的:
classTest(object):i=47def__init__(self):passt1=Test()t2=Test()#printid(t1.i),id(t2.i),id(Test.i)t1.i+=1printt1.i,t2.i,Test.i#结果是48,47,47而在Java中:
classMyTest{staticinti=47;}publicclasstest{publicstaticvoidmain(Stringargs[]){MyTestt1=MyTest();MyTestt2=MyTest();t1.i++;System.out.printf("%d%d%d",t1.i,t2.i,MyTest.i);//结果是484848}}
■分析一下HelloWorld程序
//test.java//importjava.util.*;publicclasstest{publicstaticvoidmain(Stringargs[]){System.out.println("Hello,World");}}
上面这个是Java的HelloWorld程序。首先这个文件名是test.java,所以在这个文件中的唯一一个public的class名字一定得是test。(至于什么是public后面再说)其次,public的class里面一定要有main函数,main函数是程序的入口,一整个项目之中有且只有一个。Java编译器规定main函数一定要接受Stringargs[]这样一个数组,表示是外界传递给程序的参数,类似python中的sys.argv。System是java.lang中的一个类,它含有一个静态属性,也是一个对象out,所以可以通过类名System来直接调用out。out这个对象还含有println这个方法,用于向stdout输出信息。
*关于编译和运行:在windows上用IDE写这种小程序的话,直接点击IDE里面的运行就可以得到stdout的输出了。但是如果在linux上用vim写的程序呢,就要用javac命令编译,把.java文件编译成.class文件,然后再用java命令运行.class文件即可。
■关于操作符
操作符就不多说了,主要把几点和python中不太一样或者出乎意料的东西记录一下。
●关于赋值语句操作引用
其实这个特性,java还是更加接近Python而非C语言一点。如下:
●其他一些操作符说明
顺便说下,判断两个变量是不是引用了同一个对象可以用==操作符或者equals方法。而对于基本类型,没有equals这种方法的,只能用==来判等。相比较于python中的is关键字和==操作符,可以认为,java对于基本类型,==就是对数值的一个判等操作(反正不知道地址,无所谓is了。事实上Java的基本类型都隶属于Value类,而Value类是没有equals方法的),而对于非基本类型的变量而言,==和equals都是判引用是否相同。
关于逻辑操作符这里还要说一下,首先Java中的逻辑操作符是&&,||和!。三者只能操作与布尔类型的对象,也就是说没有python中那种returnAorB方便的形式了。然后逻辑操作运算返回的也是true或者false而不是某个被操作的对象的值了(在python中1or0返回的是1而不是True)。
三元操作符是:,习惯了Python那样的if/else表示的三元操作符之后觉得确实这方面python要简单易懂很多。。需要注意的是Java中要求三元操作符的首个表达式一定要是返回boolean类型的,另外逻辑结构上三元操作符和if/else结构是一样的,不过会返回一个结果。
关于加号,Java中和Python一样允许用加号来对字符串做增长处理。对于"a"+"b"这种,当然是理所当然的。然而Java中更加自由的一点是可以允许字符串类型和非字符串类型的变量相加,编译时后者将会被强制转化成字符串。相当于"a"+b就等于是"a"+b.toString()。另外还要注意,字符串的加和也是符合加法的基本规则的。比如str+(int+int)的情况时,int和int之间的相加仍然是数字之间的相加,做完括号内加法后再和字符串相加。
■类型转换说明
类型转换也算是一种操作符,在Java中,做类型转换的格式是(cast)var。比如(String)1,(int)'1'等。这里首先需要注意的是,Java中单双引号有区别了!单引号只能引起一个字符,是char类变量,而双引号引起多个字符,是String类型变量。其次,char类型变量可以经过(int)转换成int类型变量,但是和python的int("1")完全不是一回事。如果记性好的话说不定还记得大一是学的C语言时学过ASCII基本字符表,那里面每个字符都是有数字编号的,一共127个,这个字符转换是以这个为基础的!!比如(int)'1'得到的结果是49,(int)'a'得到的结果是97等等,总之要记住,又是一个和Python很大的不同。另外String类型比如"123"是没有办法直接转化成int类型的123的。
总之,强制进行类型转换的时候,Java不能像Python那样神通广大,只有在真的转换不过去的时候才报错。Java在类型层面上就规定了哪些可以转换成哪些类型。
另一方面,Java的类型转换分成两种,将信息更多的类型转换成信息更少的类型比如float转换成int,double转换成float等属于窄化转换,是危险的。当面临这种潜在的信息丢失的时候,编译器会强制我们进行显式地写出类型转换,否则报错,以提醒这种操作存在一定的危险性。比如floatf=0.5会报无法将double转化成float,要么加上(double)要么在0.5后面加上f(Java中的小数默认是double类型而非float)。当把信息少的类型转化成信息更多的类型的时候,叫做扩展转换,此时由于不造成信息的丢失,不必一定显式地写出类型转换过程。编译器也会自动帮我们做好转换。在各种类型中,boolean类型无法和其他类型互相转换,而我们自定义的类在一般情况下也无法和其他类互相转换。
●转换过程中的小坑
1.截尾和四舍五入
和包括Python在内的诸多语言一样,Java对把float或double等带小数的数字强制转化成int类型的时候,进行的工作是截尾。也就是说9.9会被处理成9而非10。如果想要四舍五入可以调用Math.round()方法。Math类现在已经包含在java.lang中,所以不必单独的import了。
2.提升小类型到int
在对比int类型还要低的类型(short,char和byte)进行计算时,总是默认把这些类型的数据转换成int型再来计算,因此得出的计算结果必然是int及以上级别的。如果需要把计算结果赋回原来那样的类型的话就不得不面临一个信息丢失的风险了。实际上这是一个更大规律的一部分,即所有计算碰到类型不一样的类型时,往往需要进行类型转换,把较低的类型转换成较高的。因此最终的计算结果总是由整个算式中类型最高的数据来决定。
3.没有sizeof了
熟悉C/C++的人可能需要对sizeof非常敏感,因为C语言的不同数据类型根据机器的位数不同也会不同,同一个程序要移植到不同环境中去的话就需要在程序中动态地控制一些东西。而Java没有这个烦恼,因为Java程序统一是在JVM上面运行,数据类型的长度是完全统一的。因此也就不需要sizeof这个函数了。
■流程控制的语法
因为Java也是用C语言实现的,所以其流程控制的语法还是和C语言很像的。大部分熟悉的语法就不再多讲了。下面讲几个感觉新鲜的
●foreach语法
以前一直以为Java的for循环只有for(i=1;i<=10;i++)的做法,想果然还是python方便。但其实现在Java也提供这种类似于for/in的循环方法了,格式是:
floatf_array[]=newfloat[10];Randomran=newRandom();for(inti=0;i<10;i++){f_array[i]=ran.nextFloat();}for(floatf:f_array){System.out.println(f_array[i]);}//可以打印出是个随机的小数
Java中的for(floatf:f_array)相当于python中的forfinf_array,只不过
3.第一条中的可迭代对象打了引号,因为Java中没有可迭代对象这种概念的,只能迭代各种形式的数组。比如对一个字符串迭代时不能直接for(charc:"hello")而是要调用一下String类的toCharArray()方法。比如for(charc:"Hello".toCharArray())。
另外如果需要while(True)这种无限循环的话可以写for(;;)也是可以被承认的。
●return语句
Java中return语句返回的东西应该和定义方法时定义好的返回值类型相一致。如果定义的返回值类型是void的话,那么可以不在方法体中写return,会自动在方法的结尾加上一个隐式的return;。不过不建议这么做。最好能够保证每一条语句分支都能有明确的return语句和值。
●switch-case语句
switch-case语句是多少次想python中有它了。。Java中的switch-case的语法是:
switch(integer-selector){caseinteger-value1:statement;break;caseinterger-value2:statement;break;//...default:statement;}
正如之前学C时留下的映像一样,case子句中如果没有break则会按照顺序一路执行下去。直到遇到break或者default子句。
■类的初始化
Java的类的初始化方法也被称为构造器或构造方法,构造器必须和类名同名因此不遵循驼峰法则。如果类定义时不显式地定义构造方法的话,那么会默认该类可以使用一个无参构造方法。如果定义了一个构造方法,其可能有参或无参,那么这个类在实例化的时候一定要遵循定义的这个构造方法的节奏来走。方法有一个参数就一定要一个参数,两个就一定要两个。也可以在一个类中定义多个构造方法,这样就允许多种途径来创建类的实例。
构造方法是特殊的,因为它没有返回值。这个和返回值是void的方法还不一样,我们不需要也不能在构造方法中写return,同时在调用构造方法的时候会返回这个类的一个新对象的引用但是这个返回操作不需要我们在构造方法中写出来。同时,调用构造方法不需要显式指定任何对象,所以说所有构造方法也默认是静态方法。
●关于方法重载
Java允许类中进行方法的重载。重载既可以针对父类中的同名方法,也可以针对本类中已经定义的同名方法。在重载方法的时候,只要保证规定的参数不同(个数、类型、顺序上的不同都可以。顺序的不同不建议这么做,会很难懂代码),那么就允许有同名的方法出现。在调用的时候只要注意参数该怎么写就可以了。Java的做法就是根据一个独一无二的参数列表来从一些同名的方法中找出一个特定的方法。也许有人觉得返回值的不同也可以作为分别同名方法的一种方式,但是并不是。因为在调用的时候,可能不把返回值赋予变量或干嘛,总之是不好的。
*在调用方法时,可能会遇到一种情况就是给的实际参数类型和方法定义时的形式参数类型不同,此时就涉及到了类型转换。正如前面所说的,扩展转换由Java自动执行,窄化转换Java不允许,会报错。
●this关键字
在被调用的方法中如果想要引用调用它的对象,那么可以使用this关键字,这个参数是由Java隐式地传递给构造方法的。this常常用于构造方法形参和成员变量重名的情况。比如classPerson中有成员变量Stringname,intage,而构造方法又是Person(Stringname,intage)的话,那就可以在这个构造方法中写this.name=name,this.age=age。前面this.xx指代成员变量,后面的指代形式参数的值。另一种常用this的场景是当某个类的成员方法要求对调用它的类实例本身进行一些处理。
上面的两种调用this的方式都是讲this看成一个对象而言,如果在this后面加上了括号和参数,那么this就有了不同的含义,即代表了当前类的构造方法。比如我们可能在类中定义好几个构造方法而其中的一些可以调用另一些的操作。这样的话就可以直接在那些构造方法中直接用this(一定的参数)来调用其他构造方法了。
说到this在类中的应用,就不得不提static这个关键字了。在前面已经说明,被static修饰的变量和方法将具有静态的属性,即使在类没有被实例化的时候也在内存中占有空间。正是因为静态的属性可以不通过类的实例来调用,所以就不可以在static的方法中引用this关键字。事实上,static方法中不能直接引用任何非static的东西,除非通过某个实例对象的引用等办法来做,但是除了main方法之外,其他的静态方法如果需要这样来做的话显然有些多此一举了,你可以直接写一个引用的那个实例对象的类的成员方法,因为在一般的非静态方法中是允许调用静态变量和方法的。
有人认为静态方法和变量是不够“面向对象”的,当程序中出现了过多的static方法和变量时就应该反思是不是设计上不太合理。
●成员变量初始化
publicclasstest{inti;testt;//如果这里换成testt=newtest();那么下面的调用会报错,因为无限递归了。voidprint_all(){System.out.println(this.i);System.out.println(this.t);}publicstaticvoidmain(Stringargs[]){testte=newtest();te.print_all();}}//结果是0和null另外如果是数组初始化比如inta[]=newint[10];,看起来似乎是无初值初始化,但是真的无初值的话应该就是inta[];此时变量a的值是null。而前一种new的时候数组规定的构造方法是给数组元素赋初值的,规律和类成员变量赋初值一样,这里就是10个0了。这里比较容易混淆的。类初始化的时候,对于所有类中非静态的成员变量,按从上到下的次序依次执行初始化。
●成员方法和构造方法的初始化
●静态数据的初始化
static修饰的变量或者对象如果没有赋初值,那么基本类型默认是那些默认值,自定义类型默认是null。因为静态变量和方法在整个程序运行的过程中在内存中占有固定的一块地方,跟类是否进行了实例化无关,所以静态的数据的初始化必然是要早于非静态数据的初始化的(要是从头到尾都没有实例化的话,它们有可能甚至都不初始化)。具体的顺序应该是静态变量,静态方法(包括构造方法),非静态变量,非静态方法。这里可能有人会问,如果非静态变量处于静态方法中或者静态变量处于非静态方法中该怎么算。首先处于静态方法中的一定是静态变量,是有隐式的static修饰的,所以不存在前者。至于后者也不用担心,Java规定了静态变量只能存在于类的成员变量,而不能是方法中的局部变量。
因为静态数据的初始化总是早于非静态数据的,所以很自然的,静态方法中不能引用非静态的变量或方法。因为你调用这个静态方法时万一连一个实例都没创建,那非静态数据的具体取值该找谁要?
●数组初始化
正如前面所说,数组的初始化可以通过类似于inta[]=newint[10];或者int[]a=newint[10];的方法得到被默认值初始化的数组。如果想要手动初始化也可以inta[]={1,2,3,4,5}或者inta[]=newint[]{1,2,3,4,5},这里中括号中不能填具体长度且右边数据用大括号括起来。和C语言中类似,Java中的数组对象其实质是一个地址,所以在赋值如b=a之后,在数组b上做修改会反馈到数组a中。另外Java中的数组是个类,所有数组实例都隐式地含有length这个成员变量以告知用户这个数组的长度是多少。这个长度是自然长度,从1开始而不是index的从0开始计数。
这里再小介绍一下数组的一个方便的方法,是java.util中Arrays类的toString(一个数组对象)。和基本类型不同,数组类型的toString是要这样调用的。然后调用出来的结果就是像python列表那样表示的一个数组字符串了。
●接受可变参数列表
有些方法,我们在定义的时候可能并不知道调用的时候会传进来多少的、怎样的参数。这时就可以用到可变参数列表。就像python中的deff(*args,**kwargs)一样,Java中的可变参数的表示是voidf(Object...args)。Object在这里是基类,这里默认是接收一个基类的数组,因为Java中的所有类都是继承于基类的,所以所有类型的参数都可以被囊括进来。然后在方法体中,就可以像处理数组一样处理args这个参数了。比如下面这段代码:
publicclasstest{staticvoidmethodTest(Object...args){for(Objectobj:args){System.out.println(obj);}return;}publicstaticvoidmain(Stringargs[]){methodTest(1,1.5,"Hello",'c');}}/*结果就是:11.5Helloc*/
这里还需要提下,跟python中的动态参数列表不同的是,Java中的这个动态参数还是可以指定类型的。上面用Object...args实际上是属于最大尺度地接受参数,即什么都行。如果写成Character...args或者int...args之类的话就可以只接收一种类型的参数了。另外,在动态参数前面加上不同类型的静态参数还可以重载出不同功能的同名方法。
●枚举类型
enumDegree{NOT_CLASSFIED,LOW,AVERAGE,HIGH,DISASTER}
publicclasstest{publicstaticvoidmain(Stringargs[]){Degreed=Degree.AVERAGE;System.out.println(d);//输出是AVERAGE,也就是说print的时候默认是字符串形式for(Degreed:Degree.values()){System.out.println(d+"="+d.ordinal());//结果是打印出来了每个等级对应的真实值,也就是说明了values()返回的是常量对象的列表而对象.ordinal方法是返回了对象的真实值。}}}
可以想到,其实enum类型和switch语句的配合肯定是很好的。
■垃圾清理与垃圾识别
首先应该明确,垃圾识别和垃圾清理是两个不同的概念,这一整个过程可以叫垃圾回收。垃圾识别是通过一定的方法从内存中找到没有用的对象即所谓的垃圾,垃圾清理是指把这些对象占据的内存空间释放。
在Java的类中,允许定义一个名为finalize()的方法,这个方法的作用是,当Java的垃圾回收器试图清理这个类的实例时,将首先调用实例.finalize()方法。这个过程中垃圾回收器并不立刻回收对象的内存,事实上最后对象可能甚至不被清理。会这么说主要是基于两点Java垃圾回收器的特点。
第一,垃圾回收器不会每时每刻都监听要回收什么东西,它只在一些特殊的情况下,比如内存接近满了或者其他什么情况下,由JVM层面调用它,开始识别需要回收的垃圾。这主要是因为有些程序比较小的话没必要总是进行垃圾回收,与其在辨别垃圾和进行回收上面花费资源不如就让它放在那里,反正程序结束之后所有资源都会被操作系统回收。
第二,找到垃圾之后,垃圾回收器也不是立刻进行清理,而也是等待一个合适的时机去释放。这个和第一条一样,由虚拟机来判断。
综合上面的垃圾回收器的特点来看,finailize()方法似乎非常重要。然而在Java中所有类的父类——基类中已经实现了这一方法,所以大多数情况下我们可能并不需要显式地重载这个方法来达到垃圾回收的目的——基类早就帮我们做好了这件事。有时候重载这个方法的目的是为了进行垃圾回收条件的验证,比如按书上说的,我定义了一个Book类,其中有一个checkedOut属性,从逻辑上来说只有当这个属性是false的时候这个对象才应该被销毁,那么我们就可以重载finailize方法,在其中对这个条件做一个判断,如果这个属性还是true,却被识别成垃圾要进行清理时就抛出一个异常,当然最后也别忘了要在后面调用一下super.finalize()来使得没有异常情况发生时对象确实得到销毁。此时的finailize方法就像是一个垃圾回收的回调点,当因为程序员的一些疏忽忘记把一些对象的checkedOut属性设置为false以标记其失效的话,在垃圾回收的时候就会抛出这个异常来提醒程序员这里有bug。
在程序中显式地调用System.gc()可以强制要求系统进行垃圾识别与回收。
以上其实主要讲了垃圾是在何种时机下以何种方法被清理的,那么垃圾又是如何被识别出来的?
首先是“引用计数”模式的内存管理技术,这种在python中被广泛应用的技术并没有被Java采用,因为1.循环引用的问题依然存在,而且相比于python,Java是个完全面向对象的,对象间的循环引用估计不在少数。2.因为Java要求的动态性没有python那么强,引用计数的动态管理内存优势不明显。
值得一提的是,Java中两种模式的内存管理技术会在合适的时候由虚拟机自行切换以保证程序运行整体是处于高效率状态的。这也给了Java内存管理方面“自适应”的特点。而且在复制-删除时,复制到新内存空间中的有效对象们会一个挨一个地排列,这还提高了访问的效率。综合下来,Java的内存访问和分配速度也不逊色于C++这些语言。
【访问权限控制】
■包
在正式接触权限访问控制前应该需要了解包是什么。从逻辑结构的层面上来看,包就是一群类的集合。正如前面说过的java.util就是一个包,而如果要用到包中的类就可以用import语句来导入。和Python的规则类似的,*代表一个包中所有的类。所以我们可以importjava.util.*;之后,包中所有类就可以直呼其名了。(当然就java.util这个包而言没必要这么做,每个Java文件都隐含这句语句了)
为了区别于Python这里还要讲到的一点是,Java中的import只能import类,不能import一整个包或者import类中的一个静态方法(普通的成员方法就更不要说了)。如果单独想要导入一个静态方法,不想在前面写上类名来引用的话可以考虑这样的关键字:importstatic。importstaticsome.package.class.staticMethod之后,staticMethod就可以像是系统自带的一些方法一样直接调用了。
●通过CLASSPATH来找类
关于Java和Python在import机制上的一个区别就是,python在import的时候搜索的是sys.path,这是一个由python解释器自己维护的“环境变量”。而Java运行时搜索class文件的路径由系统的环境变量CLASSPATH决定。所以java中不存在什么相对导入,我要使用别的类时,不论身处何处,只要是在这个系统中,可以解析CLASSPATH这个系统变量的话,就一定得和其他所有java文件一样进行类的导入工作。比如teacher.java和student.java两个文件都处在%CLASSPATH%/school包中。如果想要在teacher.java中importstudent.java中的类,Python中的话因为他们在同一个目录下,可以直接引用,但是Java中必须写importschool.student.*;才可以。这是特别需要注意的。
●用*时的困惑
刚才说了对于引用者而言,包可以区分同名类。但那个前提条件是用完整路径来应用。比如importschool.student.thirdGrade和school.teacher.thirdGrade之后,使用时得呼呼啦啦这一大串全写上。如果是importschool.student.*和school.teacher.*的话,使用thirdGrade这个类时就无法区分它到底来自哪个类了。所以执行的时候会报错。
和python中类似的,除非确定没有重名冲突,可以使用*符号来批量地导入一批类,否则宁可麻烦一点写全名吧。
●自己造个小轮子
在掌握了以上对包的认识之后,下面我们可以自己来写个小轮子。比如按照书上那样,每次向stdout输出时要调用System.out.println()太麻烦了,可以把它包装成一个更加简洁的print方法。具体做法就是比如说在CLASSPATH的某个目录下建立一个我们自己的包,然后在里面建立文件:
packagefranknihao.tools;publicclassmytools{publicstaticvoidprint(Objectobj){System.out.println(obj);}publicstaticvoidprint(){//因为参数不同,所以是两个不同的方法System.out.println();}publicstaticvoidprint(Object...objects){//接收多个参数,然后把它们用逗号隔开地打印出来for(inti=0;i 之后在其他的文件中,我们可以importstatic这些方法,然后在程序中就可以方便地用print方法啦。比如: packagemytest;importstaticfranknihao.tools.mytools.*;publicclasstest2{publicstaticvoidmain(Stringargs[]){print("Hello,World");print();//输出一空行print(1,1.5,'c',"hello");}}/*输出结果Hello,World1,1.5,c,hello*/ ■访问权限控制 经过上面对包的讲解之后,我们肯定会想到一些权限方面的问题。比如说刚才造的小轮子,是不是在任何地方的任何java文件都可以import这些方法呢?如果不想这么做该如何做出限制?其实做法非常简单,就是public,protected,缺省和private这几个权限修饰符。权限修饰符加在类的成员变量、成员方法以及类本身前面,用于指示这个变量/方法/类可以被哪些文件访问。访问这个词比较模糊,对于类而言,就是说能不能import并且new个对象,对于方法而言可能是说可不可以通过方法所在类new出来的对象调用(可能这个对象能调用一部分方法而不能调用另一部分无访问权限的方法),对于变量而言,也是和方法类似的。 下面我们将逐个进行分析,说明每个权限的具体权限范围有多大。 ●缺省权限 缺省权限的另外一个特点就是不需要import。比如一个包中的文件A.java中有publicclassA。那么在同一个包中的B.java或其他任何文件中都可以直接在代码中引用Aobj=newA()而不需要importpackage.A。因为他们是缺省权限的。 ●public权限 ●private权限 ●protected权限 protected也是一个不属于类,只属于变量和方法的权限。其含义是被修饰的方法或变量只能在包内以及当前类的所有子类实现中才能访问。因为重点强调了子类,所以protected权限也被称为继承访问权限。这里说的所有子类是包括非本包内的子类的,所以protected权限有点像是一个缺省权限加上阉割版的public权限后的产物。 ●小总结 上面介绍的四种访问权限,从开放到封闭的顺序来看依次是public,protected,缺省,private。其中成员变量和成员方法可以用四种全部修饰符,类只能使用public和缺省两种。权限的主要作用,从设计模式上来说主要是为了“封装具体实现”。正如上面所说的,把一些过程和变量通过private等低级权限修饰符封装在类当中,然后留出高级权限符修饰的一些接口供外部调用。如此就可以做到封装起来一个类的功能,这样既安全又方便。 权限总是以更高一级的元素的权限为准。比如一个类是缺省权限的,尽管里面有public的方法或者变量,但是另一个包的文件没法访问它们,毕竟你连这个类本身都访问不了的话就没什么意义了。但是public的static的方法却是一个例外,包外调用者虽然无法创建包内类的对象,但是却可以通过类名来调用包内类中的静态方法。说白了,权限控制的,是对创建类对象,以及通过类对象进行类内变量和方法的调用。 ■类的继承 classCleaner{privateStrings="Cleaner";publicvoidappend(Stringa){s+=a;}publicStringtoString(){returns;}publicstaticvoidmain(Stringargs[]){Cleanerx=newCleaner();x.append("hello");print(x);}}publicclassDetergentextendsCleaner{publicvoidappend(Stringa){s-=a;}//改变一个方法publicvoidfoam(){print("inmethodfoam");}publicstaticvoidmain(Stringargs[]){Detergentx=newDetergent();x.append("Hello");x.foam();}}从上面这段代码中可以看出几个比较出人意料的地方。首先应该明确的是类的继承之最大意义在于对原类中方法的重载和补充,所以Detergent这个类对append方法进行了改造,并且补充了foam方法。另外还可以注意的一点是,所有类都是继承于基类的,而基类中有个toString方法很有用。在实现了这个方法之后,这个类的实例就可以直接和字符串类型相加减了。然后就是两个类中各有一个main方法。 以前我一直以为一个Java项目中最多就只能有一个main方法。其实不然。main方法只不过是指出了整个项目代码的入口,而一个项目可以有多个入口。当在命令行中我们编译某个类,就是寻找了这个类的main方法作为代码的入口。在每个类中都写上main方法,并且在main方法中写上相应的类的一些验证,这样这种模式就可以拿来做对类的单元测试。因为编译完成之后,即便是处于同一个java文件中的类还是会分出不同的class文件来让我们运行。 以上例子涉及到了子类对父类方法的重载和补充,那么如果调用呢?如果是通过子类的实例去调用父类的方法只要是有访问权限,那么都是可以调用起来的。如果是在子类定义中,那么只要是有访问权限就可以直接用方法的名字调用而不用类似于super.method()这样来调用。 再来考虑一下继承时权限控制的问题。如果原先父类中的成员变量或方法本来就对继承者开放那没什么可说的,但是如果原本是不开放的,比如包外的类继承本类后想用其中的缺省权限成员,这还是不允许的。因为不知道要继承我们这个类的类会在哪里,所以一般来说都会把成员变量写成private而把成员方法作为借口写成public。当然这不是绝对的,但是这种设计模式是很有启发意义的,符合OOP的初衷,即把私用的方法封装在类内,即便是子类想要继承也不让。 ●父类的自动初始化 从外部看来,子类似乎和父类的联系是部分相同的接口以及子类当中可能会有新增的成员变量和方法。但是实际上,两者之间还有一个更加紧密的联系,涉及到双方构造方法。即,子类在调用(无参默认)构造方法创造子类的对象时,会自动调用父类的构造方法来为子类对象创造一个父类对象作为子对象,通过关键字super可以调用这个子对象。直白点说,就是每次调用子类的默认构造方法时都会依级自动调用所有长辈类的方法。并且这个调用是在子类构造方法的所有代码执行之前的位置被调用的。可以看下面这个例子: classCreature{Creature(){print("creaturemethod");}}classHumanextendsCreature{Human(){print("humanmethod");}}publicclassFrankextendsHuman{publicFrank(){print("Frankmethod");}publicstaticvoidmain(Stringargs[]){Frankf=newFrank();}}/*结果creaturemethodhumanmethodFrankmethod*/以上碰到的三个构造方法都是没有参数的,试想如果父类中的构造方法全是要求有参数的,这就会导致子类在继承的时候无法自动调用父类的构造方法,因为你不知道要调用哪个,就算知道也不知道传递的参数应该是多少。此时编译器会报错,需要程序员自己来显式地调用super方法来指明具体调用父类构造方法。显式地调用super方法时一定要注意将super方法写在子类构造方法的第一句。另外super在类的方法中的一般含义是父类的一个匿名的实例对象。所以在合适的地方也可以调用super.method()来在子类代码中指定地调用父类中的某个方法。 ●代理 继承常常用在子类需要用到父类方法的场合中。但是如果子类只需要用到一部分父类开放的方法而对另一部分不需要的话,直接继承整个父类明显会带来不是很合理的现象。比如子类的调用者可以直接调用父类的方法,而这种越级越权的调用时Java要努力避免的。一种比较好用的设计模式就是所谓的代理。基本上是指下面这样: classplanes{publicvoidup(){print("up");}publicvoiddown(){print("down");}publicvoidshoot(){print("dadada");}}classairbusextendsplanes{//....//这样直接继承势必会导致客机类继承了shoot这种看起来是为战斗机准备的方法}classairbus2{privateplanesplaneContorl=newplanes();pubilcvoidup(){planeControl.up();}publicvoiddown(){planeControl.down();}}//这样通过一个private成员变量做代理,就可以实现“子类”只使用“父类”的一部分方法了。其实这不算是继承,儿算是一种设计模式把 ●@Override 这个看似像极了装饰器的东西是Java的姑且叫注解的一种语法。Override注解主要和子类重载父类方法有关。前面我们说过,对于子类继承父类之后,在子类中可以写一些和父类中同名的方法。如果参数一致,那么就认为是重写了父类的这个方法(重新实现,父类中方法的逻辑被覆盖),另外由于Java判定两个方法相同不仅仅看方法名还看方法的参数列表,所以如果参数不一致,那么就认为是子类中新增加了一个新方法。 这就隐藏了一个小bug,如果我们确实想重写方法,但是一不小心手抖写错了参数列表,或者一手抖打错了方法名,这样编译器就以为我们是想给子类增加新方法。为了杜绝这种错误,我们可以在子类重写方法前面加上@Override这个注释。加上这个注解之后,编译器在编译时就会自动帮助我们检查,如果在父类中没有找到名字和参数都相同的方法就会提醒我们这里有错,我们期望重写一个方法但是变成了新增一个方法。 ■自定义类的向上转型 我们知道子类的对象是可以调用继承自父类的方法和变量的。那么在某种场合下(比如一个方法规定的参数是父类,但是传递进一个子类实例作为参数的时候),我们怎么知道此时调用的到底是子类实例还是说java会把这个子类实例转化成父类实例呢?这个涉及到向上转型的问题。在基本类型比如int,float的场合下,这个是可以明确判断出来的,但是自定义类型涉及成员的调用使得这个判断变复杂了。对于这个问题我们可以这么说: java对类型的检查是严格的,也就是说,定义时方法接受父类对象作为参数,那么即使传递的是子类实例,我也是要把它转化成父类再执行方法的。而这个转化过程其实就是向上转型。也就是说,对自定义类型的处理是和一般类型类似的。 我们看一个例子: publicclasstest1{publicstaticvoidmain(Stringargs[]){Childc=newChild(3);c.printChildVar();}}classParent{protectedintvar;publicParent(intnum){var=num;}protectedvoidprintVar(Parentp){print(p.var);}}classChildextendsParent{publicChild(intnum){super(num);var=num+1;}publicvoidprintChildVar(){super.printVar(this);}}//结果是4不是3 ■final关键字 final关键字可以修饰数据、方法或类。 ●修饰数据时 可以看成是分成两种情况,一种情况是修饰基本类型的数据时,final表示修饰的这个变量其值不可以被改变,如果试图改变其值就会报错。由于其不会改变值这个特点,通常可以将其应用在编译时常量这种角色上。 需要注意的是值不可被改变不代表只被初始化一次,final和static两个关键字之间互相独立,可以连起来用比如staticfinal。对于staticfinal的变量,表名这个变量在初始化时有值并且不可被改变并且只初始化这么一次。这就说明程序从头到尾这个变量就定死在那里了。所以通常把staticfinal类型的变量名全大写,来表示这是一个类似于“宏”或者编译时常量的这种概念。为了让它能够被应用到全局,通常也会赋予其public权限所以最终往往都是以publicstaticfinal的形式出现的。 第二种情况,如果final出现在一个自定义类型或者复杂类型比如数组这样的前面时,表示该变量到对象的引用是不可改变的。但是对象本身的值是可以改变的。也就是说(用Python一点的代码来说)finalls=[1,2,3]的话,不能ls=[1,2,3,4]因为这改变了引用,但是可以ls.append(4)因为这是直接改变对象的值。 ●修饰方法时 final关键字还可以用于修饰方法,当修饰方法时,最首先的考虑就是让继承本类的子类无法重写final的方法,也就是说这个方法在本类和本类以下的所有子类都保持完全一致。从逻辑上来说和@override是相反的,后者是保证我们在继承时一定要重写某个方法。 所有的private方法都自动是final方法。因为这个方法无法被外界访问,更不用说继承重写了,在代码中也可以为private方法加上final修饰不过不会有任何显式效果。 ●修饰类时 final类的意义是这个类不能被继承。需要注意的是,final修饰了类之后所作的仅仅是禁止了继承而不禁止其他比如访问变量和方法等动作。final的类中的方法必然是final的,因为类禁止其他子类继承,自然就没有重载方法一说了。 ■初始化和类的加载(感觉越写越杂了。。) 书上下面的这段代码可以比较好地来说明static内容在加载时的特殊性 classInsect{privateinti=9;protectedintj;//没有初始化,基本类型默认初值为0Insect(){print("i="+i+";j="+j);j=39;}privatestaticintx1=printInit("staticInsect.x1initialized");staticintprintInit(Strings){print(s);return47;}}publicclassBeetleextendsInsect{privateintk=printInit("Beetle.kinitialized");publicBeetle(){print("k="+k);print("j="+j);}privatestaticintx2=printInit("staticBeetle.x2initialized");publicstaticvoidmain(Stringargs[]){print("Beetleconstructor");Beetleb=newBeetle();}/*outputstaticInsect.x1initializedstaticBeetle.x2initializedBeetleconstructori=9,j=0Beetle.kinitializedk=47j=39} 为什么结果是这样的?首先编译器完成所有类的编译之后首先要访问main函数,因为main函数是static的,所以要先加载Beetle类。但是又注意到Beetle类有父类Insect,所以先加载Insect。(这步向上加载的步骤是我们不可控制,程序自动执行的。且如果Insect还有父类则会一直向上追溯)。开始加载Insect类,注意到第一个加载的static内容是Insect的构造方法,但是这里是加载而不是执行所以Insect方法被加载进内存但是不会print出任何东西。 到此为止把前六七章简单过了一遍。。写得太长了,换一篇另起开头继续。。