如果把程式当成是魔法,前面几章都是基本的咒文。
到这章开始需要用到想像力了。
class(类)
class像是没有生命的模型,里面记载了一些关于物件的叙述与特徵。
透过new()方法,可以从模型中複製一或多个一模一样的物件出来,并且具有生命力,可以执行各种动作。想像一下你的玩具车或钢弹模型被附加了想像力,真正动起来的样子吧!
来看看以下範例:
class Employee { int id; void setId(int i){ id = i; } int getId(){ return id; }}
class Sample5_1 { public static void main(String[] args) { //实例化社员A Employee a = new Employee(); //设定A的社员编号 a.setId(100); //实例化社员B Employee b = new Employee(); //设定B的社员编号 b.setId(200); //请电脑告诉我们社员A和社员B的编号 System.out.println("社员A的编号:" + a.getId()); System.out.println("社员B的编号:" + b.getId()); }}
执行结果
社员A的编号:100社员B的编号:200
JAVA的物件像是黑盒子,如果要把资料存进去或拿出来,得要透过特定的方法。
通常会命名为set和get方法,利用这两个方法来从物件里存取资料。如範例中的setId()和getId()。
set资料的时候要準备引数,用来放进物件里。
void setId(int i) //指定引数为单个int型数据,void表示不返回数值。
get资料的时候不用引数,但要指定拿出资料时,资料的型态。
int getId() //没有引数,返回int数值。
在範例5_1里使用new()实例化了社员A和社员B。这两个物件複製了class Employee的所有资料及方法。在实例化之后,A和B各自保有自己的资料。
变数的使用範围(scope)
在方法内定义的变数又称为区域变数,只适用于该方法内(也就是{}的里面)。看看以下範例:
class Test { int x; public static void main(String[] args) { boolean y = true; if (y){ String z = "JAVA"; } System.out.println(z); }}
变数x的使用範围在class Test内
变数y的使用範围在main method内
变数z的使用範围在if内,所以System.out.println(z);这行因无法找到变数z,而导致编译错误。
建构式(constructor)
class除了变数及方法外,还可以定义建构式。
建构式是物件实例化的时候,会执行一次的式子(相当于将物件初期化)。
建构式长得跟一般方法有点像,透过以下特徵来区隔两者:
如果没有写建构式的话,JAVA预设会给一个空的建构式,长得像这样:Class名(){}
这就是为什么前面的案例没有给任何引数也能执行的原因。
不过一旦自己写了建构式,JAVA就不会再帮忙写空的建构式了。
看看以下範例:
class Employee { int id; String name; //建构式定义 Employee(int i){ id = i; } Employee(int i,String s){ id = i; name = s; }}class Sample5_2 { public static void main(String[] args) { //实例化员工 Employee a = new Employee(); //会因为找不到相应的建构式而报错 Employee b = new Employee(100); Employee c = new Employee(100,"Tom"); }}
从此範例可以发现,当要实例化一个员工时
若没有提供任何引数,则会因为找不到相应的建构式而报错
至少需提供int数值(id编号),此时Employee(int i)建构式会被呼叫。
若提供了int数值和String资料,则Employee(int i,String s)建构式会被呼叫。
这个特性称为overload,确保了资料不足的时候,程式仍具有通用性。
overload的特性不只适用于建构式,也适用于一般方法。如下例:
class Test { void myPirnt(){ //处理1 } void myPirnt(int i){ //处理2 }}
static修饰子
static是静态的意思,static修饰子可以放在变数或方法的前面,表示其属于class的变数或方法,而非属于实例化物件(instance)。
JVM在执行时,会先载入静态资料和class,然后执行main方法,然后进入动态的世界(实例化物件、建立非静态的变数等..)
注意:
static变数或方法于class建构时被建立。
非static变数或方法于实例化物件时被建立。
这表示非static变数或方法(后被建立者)可以访问static变数或方法(先被建立者)
相对的,在static方法中要访问非static变数或方法时,需要先实例化一个物件之后才能使用。
请看下例:
class Sample5_3 { int instanceVal; static int staticVal; int methodA(){return instanceVal;} //OK int methodB(){return staticVal;} //OK static int methodC(){return instanceVal;} //NG static方法不能直接访问instance变数 static int methodD(){return staticVal;} //OK static int methodE(){ //OK 实例化一个物件后,static方法可以访问此物件的instance变数 Sample5_3 obj = new Sample5_3(); return obj.instanceVal; }}
就如同各个物件拥有自己的数据,class也可以保存自己的数据(即static变数)。
由同一个class实例化出来的各个物件,可以共用和修改这些static变数。
※要使用class变数的时候,别忘记加上static喔!只有静态变数和方法,会在class建立时一同被建立。非静态的则是物件实例化时会被建立。
Static Initializer (类的建构式)
刚刚提到物件被实例化时,会执行建构式。而Static Initializer相当于类的建构式。
当类别初次被使用时,Static Initializer会被执行。
看以下範例:
class Foo { static { System.out.println("Foo class: Static Initializer"); } Foo() { System.out.println("Foo class: Constructor"); }}class Sample5_4 { static { System.out.println("Sample5_4 class: Static Initializer"); } public static void main(String[] args) { System.out.println("Sample5_4 class: Main Method"); Foo f = new Foo(); }}
执行结果
Sample5_4 class: Static InitializerSample5_4 class: Main MethodFoo class: Static InitializerFoo class: Constructor
以上的结果翻译成中文就是:
JVM(JAVA虚拟机)先载入了Sample5_4类,执行了他的Static Initializer。然后执行main方法。由于在main方法里new了一个 Foo物件,所以Foo类先被载入。然后Foo物件被初始化,Constructor被执行。Access Modifier(存取修饰子)
还记得main方法前面的public吗?这就是存取修饰子,可以放在类、变数及方法前面,决定这些东西的公开範围(存取权限)。
例如JAVA的实例化物件里面,通常会把变数设为private,而get,set等方法设定为public。以确保物件里面的值不会直接被外部看见或修改,而是只能透过固定的方法来访问。
class Employee { private int id; public Employee(int i) {id = i;} public int getId() {return id;}}class Sample5_5 { public static void main(String[] args) { Employee emp = new Employee(100); System.out.println(emp.id); System.out.println(emp.getId()); }}
尝试编译以上的档案会发现,System.out.println(emp.id);这行会因权限不足而报错。必须使用getId()方法才能访问id的值。
除了public和private之外,还有protected和预设值(未指定)两种。
要解释修饰子,首先得先认识JAVA的package(包)的概念。包就像是资料夹,里面存放不同的class文件。
文法如下:
package 包名;class X {…}
如果没指定包名的类,则会被JAVA归类到没有名字的包内。
关于包的详细介绍待会儿会再说明,先回到修饰子的说明。
public表示完全开放,任何别的包里的类都可使用此类、变数或方法。
protected只开放给同一个包使用,或是不同包但继承了此类的子类也可使用。
不指定,即採用预设值,只开放给同一个包使用。
private,仅该类自己可以使用。
值及物件的複製
在JAVA里面,如果把一个基本数值的资料,当作引数丢给一个方法进行处理。则丢进去的会是一个複製的新值,而非原本的数值。例如:
int num = 10;obj.method1(num);System.out.println(num); //方法外的num保持为10void method1(int num) { num += 10; System.out.println(num); //方法内的num为20}
写成以下的样子可能比较好理解
void method1(int n) { n += 10; System.out.println(n); //n为20}
也就是方法内的num其实跟外部的num没有任何关係。只是複製了其值而已。而且num作为引数被传入方法之后,其变数名也可以任意取名。
不过如果引数是物件的话,则物件本身会被传入方法内。
int[] array = {10};obj.method2(array);System.out.println(array[0]); //方法外的值也变为20void method2(int[] array) { array[0] += 10; System.out.println(array[0]); //方法内的值为20}
package(包)的建构
请看以下範例:
package chap05.com.se;class Foo { void print() { System.out.println("package sample"); }}class Sample5_6 { public static void main(String[] args) { Foo f = new Foo(); f.print(); }}
由于在java文件中指定了包的路径,因此编译和执行时也都要符合设定的路径。
首先在当前路径下,建立以下资料夹chap05/com/se,然后把Sample5_6.java档放进去。
进行编译
javac chap05/com/se/Sample5_6.java
执行
java chap05.com.se.Sample5_6
由于手动新增资料夹很麻烦,所以也可使用-d指令自动建立路径(d后面的"."表示指定当前路径为起点)
javac -d . Sample5_6.java
package的引用(import)
如果需要用到的类,在不同包里面时,就使用import指令来引用。
建立以下的範例档案:
Foo.java
package chap05.com.se.ren;public class Foo { //要给不同包的程式使用,必须加上public修饰子 public void print() { //要给不同包的程式使用,必须加上public修饰子 System.out.println("package sample"); }}
Sample5_7.java
package chap05.com.se;import chap05.com.se.ren.Foo;//import chap05.com.se.ren.*; //也可以使用"*"来引用ren包下面所有类。class Sample5_7 { public static void main(String[] args) { Foo f = new Foo(); f.print(); }}
先编译Foo.java,再编译Sample5_7.java(由于Sample5_7里面import了Foo,所以如果没先建立Foo的话会报错)
javac -d . Foo.javajavac -d . Sample5_7.java
※因中文导致编译失败请指定以utf-8编码进行编译,如下例:
javac -d . -encoding utf-8 Foo.java
编译完成后执行
java chap05.com.se.Sample5_7
以上是第五章 class定义及物件的学习心得,接下来第六章会介绍继承与多型。
参考教材: JAVAプログラマSilver SE8 - 山本 道子