零基础学Java第五节(面向对象一)

本篇文章是《零基础学Java》专栏的第五篇文章,文章采用通俗易懂的文字、图示及代码实战,从零基础开始带大家走上高薪之路!
类与对象 在哲学体系中,可以分为主体(subject)和客体(object),在面向对象的程序设计语言中,所有的要面对的事物都可以抽象为对象(object)。在面向对象的编程过程中,我们就是使用各种各样的对象相互协同动作来完成我们的程序功能。
在面向对象的语言中,所有使用中的对象都具有某种类型,这些类型之间也有层次关系,如同生物学中的门、纲、目、科、属、种一样,这种层次关系,我们可以用继承这个机制来完成。
Java语言为面向对象的语言,所有的对象都可以具有某些属性,以及具有某种行为功能。在Java语言中对象的属性用成员变量(域)描述,行为用方法描述。
类和对象之间的区别和联系就是,类是抽象的,它具有一类对象所有的属性和行为,相当于模板,对象是具体的,通过创建相应类的对象来完成相应的功能。我们在作面向对象的编程的时候,就是抽象出类的属性和行为,再创建具体的对象来完成功能。
Date类 定义
在JDK中,有一个用于描述日期的类:java.util.Date,它表示特定的瞬间,精确到毫秒。在这里我们自己定义一个简单的Date类,用来表示具有年、月、日的日期。我们先来看看如下定义怎么样?

public class Date{ public int year; public int month; public int day; }

在这段程序定义中,类定义为public,同时year、month、day也定义为public,它们是什么含义呢?
我们前面提到包这个概念,我们说,在同一个包中的类在功能上是相关的,但是在包内的这些类并非全部都可以被其他包中的类调用的,怎么区分呢?那些可以被包外使用的类,我们在定义的时候,在类名前加上public,而不能被包外使用的类,在定义的时候,不要加public。
year、month、day这些在类体中所定义的变量,我们称之为成员变量(域 field),那么成员变量前面的public的作用是说明这些变量是公开的,可以被以 对象.变量 这种形式调用,这称为成员变量的访问权限。比如本例:我们定义一个Date类型的变量d1,那么我们可以通过d1.year来使用d1这个对象中的year。在定义成员变量的时候,除了可以定义为public,还可以有protected、缺省(无权限修饰词)、private,它们的限制越来越严格,我们总结一下:
  • public:被public修饰的成员是完全公开的,可以被以任何合法的访问形式而访问
  • protected:被protected修饰的成员可以在定义它们的类中访问,也可被同一包中的其他类及其子类(该子类可以同其不在同一个包中)访问,但不能被其他包中的非子类访问
  • 缺省:指不使用权限修饰符。该类成员可以在定义它们的类中被访问,也可被其同一包中的其他类访问,但不能被其他包中的类访问,其子类如果与其不在同一个包,也是不能访问的。
  • private:只能在定义它们的类中访问,仅此而已。
在类中,我们不但可以定义成员变量,还可以定义成员方法,成员方法前面也可以有这四种权限控制,含义是一样的。但是大家千万不要以为在独立类的前面也可以有private、protected这两个修饰词,在类前只能有public或者没有,含义前面已述。
零基础学Java第五节(面向对象一)
文章图片

关于public类和源程序文件的关系,我们在以前学习过,这里再重新提一下:
每个源代码文件中至少定义一个类,若有多个类,最多只有一个类定义为public,若有public类,则该源代码文件的前缀名字要同该类的类名完全一致,若没有public类,则源代码文件的前缀名可不与文件内的任何类一致。
测试
上面的Date类已经定义好了,我们怎么测试这个类呢?我们在这个类中定义main方法了吗?没有main方法的类,可以以 java 类名 这种方式在控制台中执行吗?显然不行,那怎么办呢?有两种方案,一种是我们给它定义一个main方法;另一种方案,再定义一个类,专门测试这个Date类。我们依次来看看。
添加main方法 我们在Date的定义中加入main方法,代码如下:
public class Date{ public int year; public int month; public int day; public static void main(String[] args){ year = 2016; month = 9; day = 5; System.out.println("year:" + year + " month:" + month + " day:" + day); } }

编译...,怎么?又错了?
别慌,看看出错提示,都是无法从静态上下文中引用非静态 变量这样的提示,这是什么意思?我们看看main方法前面是不是有个修饰词static,我们前面提到凡static修饰的成员变量或者方法,都可以用 类名.成员 这种形式来访问。如果Date类在控制台中以 java Date这种命令形式来执行,我想请问,系统是创建了一个Date对象了,然后再执行这个对象中的main方法这种方式来执行的Date类吗?错,系统是直接找到Date.class这个类文件,把这个类调入内存,然后直接执行了main,这时在内存中并没有Date对象存在,这就是为什么main要定义为static,因为main是通过类来执行的,不是通过对象来执行的。
那上面的解释又同这个编译错误提示什么关系?我们看看 year、month、day前面有没有static这个修饰词?没有,这说明这些成员变量不能通过类名直接访问,必须通过创建类的对象,再通过对象来访问。而main在执行的时候,内存中并没有对象存在,那自然那些没有static修饰的成员就不能被访问,因为它们不存在,是不是这个逻辑?想明白了吗?还没有?再想想!
我们得出个能记住的结论,在static修饰的方法中,只能使用本类中那些static修饰的成员(包括成员变量、方法等),如果使用本类中非static的成员,也必须创建本类的对象,通过对象使用。好吧,不明白道理,就先把这个结论记着。
我们根据上面的结论,把测试代码再改改:
public class Date2{ public int year; public int month; public int day; public static void main(String[] args){ Date2 d1 = new Date2(); d1.year = 2016; d1.month = 9; d1.day = 5; System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day); } }

编译...,OK,运行,通过,
那如果非静态方法中使用静态成员,可不可以呢?我们说可以,简单讲,这是因为先于对象存在,静态成员依赖于,非静态成员依赖于对象
上面的程序中,我们给这几个成员变量分别给了值,如果不给值直接输出呢?我们试试:
public class Date3{ public int year; public int month; public int day; public static void main(String[] args){ Date3 d1 = new Date3(); System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day); } }

编译,运行,结果: ,都是0。这里有个结论:成员变量如果没有显式初始化,其初始值为0值,或者相当于0值的值,比如引用变量,未显式初始化,其值为null,逻辑变量的未显式初始化的值为false。这里要注意,我们说的是成员变量,如果是局部变量,没有显式初始化就用,编译是通不过的。比如上面的代码,改为:
public class Date3{ public int year; public int month; public int day; public static void main(String[] args){ Date3 d1; //这里d1为局部变量,没有初始化 System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day); } }

编译:零基础学Java第五节(面向对象一)
文章图片
从编译提示,我们知道d1未初始化就用,这是不对的。
创建一个测试类 代码如下:
//本类放在Date.java文件中 public class Date{ public int year; public int month; public int day; }//本类放在TestDate.java文件中 public class TestDate{ public static void main(String[] args){ Date d1 = new Date(); d1.year = 2016; d1.month = 9; d1.day = 5; System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day); } }

这两个类都是public类,所以,这两个类应该写在两个不同的源代码文件中,文件名分别为:Date.javaTestDate.java中。如果大家想将这两个类写在一个源代码文件中,因为Date类在测试完成以后是会被公开使用的,所以我们应该把Date定义为public,那么源代码文件的名字就是Date.java,而TestDate这个类是用于测试Date类的,所以它前面的public就应该去掉。代码如下:
//这些代码放在Date.java文件中 class TestDate{ public static void main(String[] args){ Date d1 = new Date(); d1.year = 2016; d1.month = 9; d1.day = 5; System.out.println("year:" + d1.year + " month:" + d1.month + " day:" + d1.day); } }public class Date{ public int year; public int month; public int day; }

这两个类的定义顺序无关紧要。编译后,我们在命令行上执行:java TestDate即可。
思考
封装 在这个示例中,有个问题:我们在创建Date对象d1以后,就直接使用d1.这种形式给year、month、day赋值了,如果我们给它们的值不是一个合法的值怎么办?比如month的值不在1~12之间,等等问题。在如上的Date定义中,我们没有办法控制其他代码对这些public修饰的变量随意设置值。
我们可以把这些成员变量用private修饰词保护起来。由private修饰的成员,只能在类或对象内部自我访问,在外部不能访问,把上面的Date代码改为:
public class Date{ private int year; private int month; private int day; }

这样我们就不可以通过d1.year来访问d1对象中year了。慢着,不能访问year,那这个变量还有什么用?是的,如果没有其它手段,上面的这个类就是无用的一个类,我们根本没有办法在其中存放数据。
怎么办?我们通过为每个private的成员变量增加一对public的方法来对这些变量进行设置值和获取值。这些方法的命名方式为:setXxx和getXxx,setter方法为变量赋值,getter取变量的值,布尔类型的变量的取值用:isXxx命名,Xxx为变量的名字,上例改为:
public class Date{ private int year; private int month; private int day; public void setYear(int year){ //理论上year是没有公元0年的,我们对传入的值为0的实参处理为1 //year的正值表示AD,负值表示BC if(year == 0){ //这里有两个year,一个是成员变量,一个是实参,这是允许的 //为了区分它们,在成员变量year前面加上this. //实参year不做处理 //如果没有变量和成员变量同名,this是可以不写的 this.year = 1; } else { this.year = year; } }//因为要取的是year的值,所以getter方法的返回值同所取变量的类型一致 public int getYear(){ return year; }public void setMonth(int month){ if((month > 0) && (month < 13)){ this.month = month; } else{ this.month = 1; } }public int getMonth(){ return month; }public void setDay(int day){ //这个方法有些复杂,因为我们需要根据year、month的值来判断实参的值是否合规 switch(month){ case 1: case 3: case 5: case 7: case 8: case 10: case 12:if (day < 32 && day > 0) {//在1~31范围内 this.day = day; }else{ this.day = 1; //超出日期正常范围,我们设为1 } break; case 4: case 6: case 9: case 11:if (day < 31 && day > 0) {//在1~30范围内 this.day = day; }else{ this.day = 1; //超出日期正常范围,我们设为1 } break; case 2:if (isLeapYear()) { if (day < 30 && day > 0) {//在1~29范围内 this.day = day; }else{ this.day = 1; //超出日期正常范围,我们设为1 } } else { if (day < 29 && day > 0) {//在1~28范围内 this.day = day; }else{ this.day = 1; //超出日期正常范围,我们设为1 } } break; default:this.day = 1; //如果month的值不在上述情况下,day设置为1 break; } }//这个方法判断年份是否为闰年,是闰年返回true,否则返回false //该方法只在本类内部使用,所以定义为private private boolean isLeapYear(){ //可被400整除或者被4整除但不能被100整除的年份为闰年,其它年份为平年 if((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))){ return true; }return false; //能执行这里,说明是平年 } }

经过上面的改造,虽然代码长了很多,但是安全了很多。我们对year、month、day的值不再是直接存取,而是通过相应变量的getter和setter方法来存取,这些方法在进行存取的时候,会判断设置的值是否合规。
上面的代码其实是可以优化的,比如setDay可以优化如下:
public void setDay(int day){ //这个方法有些复杂,因为我们需要根据year、month的值来判断实参的值是否合规 this.day = 1; //这里先把day设置为1,下面的超范围的情况就不用再写代码了 switch(month){ case 1: case 3: case 5: case 7: case 8: case 10: case 12:if (day < 32 && day > 0) {//在1~31范围内 this.day = day; } break; case 4: case 6: case 9: case 11:if (day < 31 && day > 0) {//在1~30范围内 this.day = day; } break; case 2:if (isLeapYear()) { if (day < 30 && day > 0) {//在1~29范围内 this.day = day; } } else { if (day < 29 && day > 0) {//在1~28范围内 this.day = day; } } break; } }

我们通过上面的示例,看到了面向对象的一个概念:封装。我们将数据用private隐藏起来,通过public存取方法对数据进行了封装,使得数据更加安全与可靠。
成员变量的初始化 非静态成员变量的初始化 在上面的例子中,如果我们新建一个Date对象以后,如果不初始化,我们可以知道,直接取其中的year、month、day的值的时候,它们的值都是0,这显然是不合理的。要解决这个问题,我们需要在创建Date对象的时候,让系统自动初始化一个合适的值,要达到此目的,我们可以采用三种方式:
  • 在定义成员变量的时候初始化值,如上面的代码,我们修改一下如下:
    public class Date{ private int year = 1; private int month = 1; private int day = 1; ...}

    上面的代码,使得新建Date对象的初始值均为1。
  • 第二种方式为使用构造方法
    构造方法是类中一种特别的方法,它的方法名和类名一致,该方法没有返回值,甚至连void都没有,而且构造方法不能被当作普通方法一样被调用,每当生成某个类的对象的时候,构造方法才被调用。构造方法的作用就是在创建对象的时候执行对对象的初始化工作。我们使用构造方法对成员变量来进行初始化,代码如下:
    public class Date{ private int year; private int month; private int day; public Date(){ year = 1; month = 1; day = 1; }...}

    上面的代码使得我们在新建Date对象时,可以初始化其内的值。上面的构造方法没有参数,这种构造方法,我们称之为默认构造方法。那是不是用构造方法只能初始化为这种固定的值呢?能不能在创建对象的时候再指定初始化值呢?答案是可以的,就是使用有参数的构造方法。代码如下:
    public class Date{ private int year; private int month; private int day; public Date(int year, int month, int day){ //下面的代码使用setter方法进行初始化是因为,setter方法提供了参数检查, //如果不用setter方法,我们就需要重新写对参数的检查代码 setYear(year); setMonth(month); setDay(day); }...}

    上面的代码使得我们可以在创建新的Date对象的时候指定初始化值,比如:Date d = new Date(2016,9,5); 。但是上面的代码没有定义无参构造方法,我们再用无参构造方法来创建Date对象就不行了,如:Date d = new Date(); ,编译时会出现如图的错误:
    这是为什么呢?这是因为,如果定义类时没有定义构造方法,则编译器会自动创建一个无参的空的public构造方法,作为该类的构造方法。但是,只要你定义了构造方法,不管是有参还是无参,编译器就不再自动生成了,就会出现上面的错误提示。
    另外,在构造方法中产生的异常会被jvm忽略,即使是在构造方法中使用try也无济于事。
    public class Test {public Test() { try { System.out.println("trying to throw an exception"); double x = 1.0/0.0; //此句会产生除0异常 } catch(Exception e) { System.out.println("Exception captured"); } finally { System.out.println("inside finally"); } }public static void main(String args[]) { Test test = new Test(); } }

    上述代码在执行以后的结果如图: 从图大家可以看到发生的异常并未被catch。
  • 第三种方式是使用实例语句块。那么什么是实例语句块呢?实例语句块是由一对大括号括起来的可以被执行的语句序列,这个语句块在类的内部,但不在任何方法的内部,如下代码:
    public class Date{ private int year; private int month; private int day; //下面的大括号为实例语句块 { year = 1; month = 1; day = 1; }...}

    上面的实例语句块会在构造方法执行前被执行。如果出现多个实例语句块,按顺序执行。
  • 如果这三种方式同时采用呢?代码如下:
    public class Date{ private int year = 1; private int month = 1; private int day = 1; { year = 2; month = 2; day = 2; } public Date(){ year = 3; month = 3; day = 3; }...}

    上面的Date类,如果要新建对象,其初始值是1、2,还是3呢?在创建对象的时候,会在堆中为对象分配存储空间再将所分配空间的所有内容都设为默认初始值0,接着将成员变量定义时的值对成员变量进行初始化,再执行实例语句块,最后执行构造方法。因此,上例的初始最终值为3 。
静态成员变量的初始化
  • 我们前面所涉及到的是对非静态成员变量的初始化,我们知道有一类成员变量是用static修饰的,这类成员变量是不依赖对象的,那么这类变量如何初始化呢?
    首先我们要明确,静态成员变量的存储空间不同任何该类的对象相关联,它们的存储空间是独立的。由此,我们可以得出两个结论,首先静态成员变量的值只能被初始化一次,其次,静态成员变量的值不能在构造方法中初始化(因为构造方法是在创建对象的时候调用的)。
    那么怎么对静态成员变量进行初始化呢?有两种方式:
    • 象初始化非静态成员变量一样,直接给静态成员变量赋初始值,如:
      public class Test{ public static int first = 1; public static int second = 2; }

    • 第二种,使用静态语句块,如下:
      public class Test{ public static int first; public static int second; //下面的语句就是静态语句块,这种语句块不能放在方法体中 static{ first = 1; second = 2; } }

      在类中,可以出现多个静态语句块,多个静态语句块顺序执行。
    • 静态成员变量的初始化发生在该成员变量第一次被使用的时候,之后该成员变量不会再重复初始化。而且类中的静态成员变量的初始化发生在非静态成员变量的初始化之前,这样,下面的代码就不合适了:
      public class Test{ public int first = 1; public static int second = first; }

类定义中的成员 Java的类中可以包含两种成员:实例成员和类成员。
实例成员
  • 实例成员(包括实例成员变量及实例方法)属于对象,通过引用访问:
    • 引用变量.实例成员名;
    • 定义类时,如果成员未被static修饰,则所定义的成员为实例成员,如:
      int i=10; void f(){…}

  • 实例成员的存储分配
    • 通常,类只是描述,通过使用new,对对象进行存储空间分配。未被static所修饰的成员是对象的一部分,因此,这种实例成员的存储分配伴随对象的分配而分配。
    • 例如:
      class T{ int i; void f(){} } 。。。 T t1,t2; t1=new T(); t2=new T();

      则此时,i有两个副本,一个在t1中,一个t2中,通过t1.i及t2.i使用
    类成员
  • 类成员(包括类成员变量及类方法)属于类,通过类名访问,也可通过引用访问:
    • 类名.类成员名;
    • 定义类时,如果成员被static修饰,则所定义的成员为类成员,如:
      static int count=0; public static void main(String args[]){…}

  • 类成员的存储分配
    • static用于成员变量前,则不管有多少对象,该成员变量只保留一份公共存储,称为类变量(静态变量);
    • 如果在定义方法时,使用static进行修饰,称为类方法(静态方法),则在调用该static方法,则该方法不依赖具体对象,既是你可以调用某个方法,而不用创建对象。
    • 对于类成员的使用,即可以使用类名使用,也可以通过引用使用,如:
      class T{ static int i=47; static void f(){i++; } } 。。。 T t1,t2; t1=new T(); t2=new T();

      则此时,i只有一份,既通过t1.i及t2.i使用,也可通过T.i引用,其值均为47。在执行T.i++; 后t1.i及t2.i的值均为48;对f的调用既可以以如t1.f(); 也可以T.f(); 方式使用。根据java编程规范,我们建议对于static成员,只采用类名引用的方式。
类成员、实例成员总结
  • 当你使用static就意味着,这个成员变量或方法不依赖于任何该类的对象,所以无需创建对象,就可使用该static数据或方法。
  • 对于non-static 数据和方法则必须创建对象,然后使用该对象操作 non-static 数据和方法。
  • 由此,因为 static 方法不需要创建对象,所以static 方法不能直接存取非static成员。
  • 而non-static 方法则可以直接调用static成员
    public class Test{ static int i=0; int j=10; static void f(){i++; } void s(){f(); j++; }public static void main(String[] args){ Test t=new Test(); t.s(); System.out.println(i); } }

  • static的一个重要应用就是无需创建对象而调用方法。比如main( )
  • 在定义一个类时,个性的定义为non-static,共性的定义为static
this 在Java中我们经常看到对this这个关键字地使用。它的使用有两种场合:
  • this作为一个引用用来引用自身,每个对象都有一个this。在类的成员方法中,this用于表示对本对象内的方法或者变量的使用。如果在不会引起歧义的情况下,this是可以省略的。比如上面的一系列setter方法中,因为参数同成员变量同名,为了以示区别,成员变量前的this就不能省略了。
    this不能用在静态方法中。因为静态方法的执行根本不需要java对象的存在,而是直接使用 类名.方法 的方式访问,而this代表的是当前对象,所以在静态方法中根本无法使用this。
  • this可以用在构造方法中,对本类中其它构造方法进行调用
    • 语法:this([实参]);
    • 作用:在一个构造方法中去调用另一个构造方法。
    • 目的:代码重用。
    • this(实参):必须是构造方法中第一个被执行的语句,且只被调用一次。
    我们读读下面的代码
    public class Flower { int petalCount = 0; String s = new String("null"); Flower(int petals) { petalCount = petals; System.out.println("Constructor with int arg only, petalCount= "+ petalCount); }Flower(String ss) { System.out.println("Constructor with String arg only, s=" + ss); s = ss; }Flower(String s, int petals) { this(petals); //调用Flower(petals),但不能写作Flower(petals) //! this(s); // Can't call two! this.s = s; // 这是this的另一种使用方式 System.out.println("String & int args"); }Flower() { this("hi", 47); System.out.println("default constructor (no args)"); }void print() { //! this(11); // Not inside non-constructor! System.out.println("petalCount = " + petalCount + " s = "+ s); }public static void main(String[] args) { Flower x = new Flower(); x.print(); } } ///:~

private构造方法 在构造方法前也可以有public等修饰词用于限定构造方法的存取权限。一般情况下,如果类是public,构造方法也是public,这样通过new才能调用。所以,如果构造方法前面被private 修饰,那会发生什么事情呢?我们在类外就不能生成该类的对象了。大家可以自己测试一下。
【零基础学Java第五节(面向对象一)】那是不是private就不能用在构造方法前面了呢?当然可以,而且这是一种很有用的情况。在有些情况下,有些类在系统中只能允许存在一个该类的实例(对象),这时,我们就可以把该类的构造方法定义为private的。示例代码如下:
public class Handler { //handler变量用来保存该类对象的引用 private static Handler handler = null; private Handler() { /* set something here */ }//该类的对象只能通过getHandler方法从handler这个私有变量中获取 public static getHandler(/* arglist */) { //如果handler值非空,说明已经在系统中存在该类的对象,直接取出,不再生成,这就保证了单例 if (!handler) handler = new Handler(); return handler; }public static void main(String args[]) { Handler.getHandler(); } }

    推荐阅读