【左京淳的JAVA学习笔记】第五章 class定义与物件生成

如果把程序当成是魔法,前面几章都是基本的咒文。
到这章开始需要用到想像力了。

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除了变数及方法外,还可以定义建构式。
建构式是物件实例化的时候,会执行一次的式子(相当於将物件初期化)。
建构式长得跟一般方法有点像,透过以下特徵来区隔两者:

  1. 建构式的名字与class一样
  2. 没有返回值
  3. 可以建立复数个名称一样,但接收不同引数的建构式。依照引数的格式,会执行相应的建构式。

如果没有写建构式的话,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 Initializer
Sample5_4 class: Main Method
Foo class: Static Initializer
Foo class: Constructor

以上的结果翻译成中文就是:

  1. JVM(JAVA虚拟机)先载入了Sample5_4类,执行了他的Static Initializer。
  2. 然後执行main方法。
  3. 由於在main方法里new了一个 Foo物件,所以Foo类先被载入。
  4. 然後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保持为10

void 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]);  //方法外的值也变为20

void 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.java
javac -d . Sample5_7.java

※因中文导致编译失败请指定以utf-8编码进行编译,如下例:

javac -d . -encoding utf-8 Foo.java

编译完成後执行

java chap05.com.se.Sample5_7

以上是第五章 class定义及物件的学习心得,接下来第六章会介绍继承与多型。

参考教材: JAVAプログラマSilver SE8 - 山本 道子


<<:  Golang-gRPC & Protocol Buffers

>>:  laravel8 10分钟保证完成

DAY27:实作专案之动机及方向

今天先跟大家说明在我的学习过程中做出的一个小小专题,这个专题在当初还有跟三个同学共同完成,那就先说明...

每个人都该学的30个Python技巧|技巧 5:各种运算子(上)(字幕、衬乐、练习)

昨天教的字串格式化你有没有学会了呢╰(*°▽°*)╯,还没学会的在等什麽,快回去复习呀!!!在此附上...

Bloom效果,又或是高光效果

文章内使用Unity 2019 LTS 目标 Bloom效果 Bloom 以下这张图片也是一个常见...

那些被忽略但很好用的 Web API / FullScreen

一起来延伸视野,迎接更大的画面吧! 今天要介绍的 FullScreen API 会被忽略的原因可能...

Day28-保护鲸鱼人人有责(三)

小结 前两天讲了几个比较重要的 Dockerfile best practice 之後,今天要来说一...