写点什么

Java 面向对象基础

  • 2022 年 7 月 12 日
  • 本文字数:5646 字

    阅读完需:约 19 分钟

面向对象基础


类与对象

在哲学体系中,可以分为主体(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 或者没有,含义前面已述。


关于 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); }}
复制代码


从编译提示,我们知道 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 存取方法对数据进行了封装,使得数据更加安全与可靠。

发布于: 刚刚阅读数: 3
用户头像

InfoQ签约作者 2020.11.10 加入

文章首发于公众号:五分钟学大数据。大数据领域原创技术号,深入大数据技术

评论

发布
暂无评论
Java面向对象基础_Java_五分钟学大数据_InfoQ写作社区