相信通过对前面几个章节的学习,对于Java言语中的数据类型、程序控制语句、循环语句、数组等比较了解了。随着计算机语言的发展,面向对象的概念孕育而生,而类(class)是面向对象程序设计中最重要的概念之一,从本章开始将展开对类的深入学习。
6.1 面向对象程序设计
早期的程序设计通常使用的是“面向过程”的程序设计方式,随着编程语言的不断发展,软件需求不断变化,开发人员已经明显感觉到“面向过程”程序设计的不足。为了能更好的解决复杂问题,“面向对象”程序设计就诞生了。面向对象的程序设计中涉及到类、对象、封装、继承及多态等。这些都会在后面的章节中一一介绍。
那么,什么是面向对象呢?
面向对象程序设计是将人们认识世界,了解世界的过程中普遍采用的思维方法应用到程序设计中。在Java中提倡万物皆对象。对象是现实世界中存在的事物,它们可能是看得到摸得到的有形事物,如书、电脑、汽车等;也可能是看不见摸不着的无形的事物,如空气、计划、想法等。将一些具有相同属性或行为的对象向上抽取出来形成类。简单的描述,具有共同性质的事物的集合就可以称为类。
在面向对象程序设计中,类是一个独立的单元。类是一个抽象的概念,要利用类的方式来解决问题,必须用类创建一个实例化的对象,然后通过这个对象去访问类的成员变量或调用成员方法来实现程序需要的功能。
6.2 定义类
汽车生产厂商在生产一款汽车之前,首先会画一张汽车设计图,设计图上标明汽车的外形尺寸、汽车颜色、轮胎大小等等,然后厂商再根据这张设计图来生产出真正的汽车,并给这些汽车取一些名字。
对于程序设计来说,汽车的设计图就相当于类(class),根据设计图生产出来的一辆辆汽车就是对象。如果用Java来定义这个类的话,代码如下所示:
class Car{ String color; float width; float height; }
定义类需要使用关键词class。class后面的Car是类的名称,在后面使用该类时需要引用这个名称。字符串类型的变量color用来定义汽车的颜色,而浮点数类型的变量width用来定义汽车的宽,浮点数类型的变量height用来定义汽车的高。而color、width、height这三个变量在Java中称之为是类Car的成员变量。
类定义好后,目的是为了使用这个类。而要使用这个类,必须使用关键词 new 来创建一个实例对象。换句话说就是关键词 new 就是用来创建类实例的。例如:
Car car = new Car();
通过上面代码的定义,就把汽车类型的变量car指向了类Car的内存地址。结构图如下所示:
范例:Car.java
public class Car{ String color; float width; float height; public static void main(String[] args){ Car car = new Car(); car.color = "白色"; car.width = 6.72f; car.height = 7.53f; System.out.println("color: " + car.color + ", width: " + car.width + ", height: " + car.height); } }
程序输出结果如下:
color: 白色, width: 6.72, height: 7.53
在类Car的main()方法中,创建了一个Car的引用实例对象car,然后通过对象car来对类Car中的成员变量进行赋值操作,然后再通过对象car来进行输出。通过实例对象该类成员时,是通过点(.)操作符来完成的。语法格式如下:
实例对象名称.类成员变量(或成员方法);
6.2.1 构造方法
从上面的范例代码中不难发现,在给类成员变量赋值之前先做了实例化对象操作。如果想在实例化对象的同时就能给类成员变量赋值能否办到?如果能办到那又该如何实现呢?
这是能办到的,在Java中提供了构造方法(Constructor)来实现这个功能。
那么什么是构造方法?
构造方法就是方法名称必须与类名称相同,并且没有返回值类型的方法。构造方法的作用就是为对象进行实例化。使用构造方法后的示例代码如下:
范例:Car.java
public class Car{ String color; float width; float height; //定义构造方法 public Car(String color, float width, float height){ this.color = color; this.width = width; this.height = height; } public static void main(String[] args){ Car car = new Car("白色", 6.72f, 7.53f); System.out.println("color: " + car.color + ", width: " + car.width + ", height: " + car.height); } }
程序输出结果如下:
color: 白色, width: 6.72, height: 7.53
在构造方法中,使用了this.color = color;语句,这里的 this 是一个关键词,代表的是Car这个类实例代后的引用对象。
不难看出两者的输出结果是相同的,但是程序代码的实现确有区别。在第一个范例中是通过Car car = new Car();来实例化对象的;而在第二个范例中是通过Car car = new Car("白色", 6.72f, 7.53f);来实例化对象的。一个无参数,一个有参数,这和前面讲到的方法重载相符合。由此说明构造方法也能重载。那么现在再一次调整Car.java类,代码如下:
范例:Car.java
public class Car{ String color; float width; float height; //定义构造方法 public Car(String color, float width, float height){ this.color = color; this.width = width; this.height = height; } public static void main(String[] args){ Car car1 = new Car(); System.out.println("color: " + car1.color + ", width: " + car1.width + ", height: " + car1.height); Car car2 = new Car("白色", 6.72f, 7.53f); System.out.println("color: " + car2.color + ", width: " + car2.width + ", height: " + car2.height); } }
在编译这段程序时,发生出如下错误:
为什么会发生错误呢?第一个Car程序中就使用过new Car();为什么这个范例中不能使用new Car();呢?
原因就在于本范例中已经有了一个构造方法,而且这个构造方法是一个带参数的构造方法。当Java类中没有提供构造方法时,Java虚拟机(JVM)会自动给该类提供一个无参的,不做任何操作的构造方法,目的是为了该类能实例化;而当类中提供了别的构造方法后,Java虚拟机就不再提供无参的构造方法了,这时如果想要使用无参构造方法,就需要显示的定义。所以上例代码修改如下后就可以正常通过编译。
范例:Car.java
public class Car{ String color; float width; float height; //定义无参构造方法 public Car(){ } //定义构造方法 public Car(String color, float width, float height){ this.color = color; this.width = width; this.height = height; } public static void main(String[] args){ Car car1 = new Car(); System.out.println("color: " + car1.color + ", width: " + car1.width + ", height: " + car1.height); Car car2 = new Car("白色", 6.72f, 7.53f); System.out.println("color: " + car2.color + ", width: " + car2.width + ", height: " + car2.height); } }
程序输出结果如下:
color: null, width: 0.0, height: 0.0
color: 白色, width: 6.72, height: 7.53
本范例使用了构造方法重载,同时创建了多个实例对象。它们的内存分配结构图如下所示:
注:当编写有参构造方法时,最后提供一个无参的构造方法。这样可以避免一些错误发生。
6.3 this和static关键词
6.3.1 this关键词
this关键词表示当前对象的引用。所谓当前对象指的是调用类中方法或属性的那个对象。this关键词可以访问属性、调用方法(包括调用构造方法)、表示当前对象。
1)访问属性
范例:Person.java
public class Person{ String name; int age; public Person(String name, int age){ name = name; age = age; } public void print(){ System.out.println("name="+name+",age="+age); } public static void main(String[] args){ Person p = new Person("lisi", 20); p.print(); } }
本例输出结果为:
name=null,age=0
为什么结果不是name=list,age=20呢?
本例是想通过Person类的构造方法来为Person类的name属性(变量)和age属性进行初始化,但在构造方法中声明的两个参数名称也同样是name和age,而且它们是局部变量。当局部变量与类属性相同时,会覆盖类属性。在构造方法中使用的是name = name;来赋值,这就相当于把参数列表中的name值赋给了参数列表中的name自己,而并没有把参数列表中的name赋给Person的成员变量name。为了解决这个问题,程序代码修改如下:
范例:Person.java
public class Person{ String name; int age; public Person(String name, int age){ this.name = name; this.age = age; } public void print(){ System.out.println("name="+name+",age="+age); } public static void main(String[] args){ Person p = new Person("lisi", 20); p.print(); } }
通过this.name和this.age来显示指定是把参数列表中的name变量的值赋给Person类中的name属性;把参数列表中的age变量的值赋给Person类中的age属性。程序的输出结果自然就是想要的结果了。而这里的this关键词就表示当前对象(Person对象)。
2)调用方法
范例:ThisDemo01.java
class Person{ String name; int age; public Person(){ System.out.println("无参构造"); } public Person(String name, int age){ this(); System.out.println("有参构造"); } } public class ThisDemo01{ public static void main(String[] args){ Person p = new Person("lisi", 20); } }
本例输出结果为:
无参构造
有参构造
在有参构造中通过this()来调用无参构造。当通过this(参数列表)来调用构造方法时,该条语句必须放在构造方法的第一行位置。需要注意的是,如果一个类中有多个构造方法,那么必须有一个不能使用this来调用其它构造方法。
3)表示当前对象
this的最大用法实际上就只有一个“表示当前对象”,包括以上的访问属性和调用构造方法。当前调用类中方法的对象称为当前对象。
范例:ThisDemo02.java
class Person{ public void show(){ System.out.println("当前对象:" + this); } } public class ThisDemo02{ public static void main(String[] args){ Person p1 = new Person(); p1.show(); System.out.println("----------------"); Person p2 = new Person(); p2.show(); } }
本例输出结果为:
当前对象:Person@58e4d7f7
----------------
当前对象:Person@181ec6b9
6.3.2 static关键词
static关键词在类中可以修饰属性和方法。被static修饰的属性称为类属性,变static修饰的方法称为类方法。static关键词的作用是实现数据共享。
1)修饰属性
范例:StaticDemo01.java
class Shape{ static int num = 0; public Shape(){ num++; System.out.println("创建了 " + num + " 个对象"); } } public class StaticDemo01{ public static void main(String[] args){ Shape shape1 = new Shape(); Shape shape2 = new Shape(); Shape shape3 = new Shape(); } }
本例输出结果为:
创建了 1 个对象
创建了 2 个对象
创建了 3 个对象
从本例输出结果可以看出,这几个对象共享了num属性。
2)修饰方法
范例:StaticDemo02.java
class Shape{ public static void show(String str){ System.out.println(str); } } public class StaticDemo02{ public static void main(String[] args){ Shape shape = new Shape(); shape.show("Hello"); Shape.show("World"); } }
对于static修饰的方法,可以创建对象后再调用方法,也可以通过类名.方法名称的方式来调用,推荐使用后者。
注:被static修饰的属性或方法是优先于对象存在的,它的加载时机要比对象早,因此,被static修饰的方法中不能使用this关键词。因为this必须要创建完对象后才能使用。另外static关键词不能修饰局部变量。
其实Java程序执行的入口方法main()方法就是一个静态方法。之所以main()方法要声明为static,是因为Java虚拟机在执行main()方法时不需要创建对象。而main()方法是程序的执行入口,不需要有返回值,所以使用void来修饰,另外方法有可能带有参数,所以使用String类型的数组来进行接收。
6.4 字符串对象
在Java中,字符串是java.lang.String类的实例,由于这个类使用频率非常高,Java赋予了它一些特定的方法。
6.4.1 字符串基础
由字符组成的文字符号称为字符串。字符串"Java"由'J'、'a'、'v'、'a'四个字符组成。在Java语言中字符串是以""来定义的。
String name = "zhangsan"; System.out.println(name); System.out.println(name.length()); System.out.println(name.charAt(1)); System.out.println(name.toUpperCase()); System.out.println(name.substring(1,3));
第 1 行代码定义了String类型的变量name,值为zhangsan;第 2 行代码是输出变量name的值——zhangsan;第 3 行代码显示了字符串“zhangsan”的长度为8;第 4 行代码显示第2个字符h;第 5 行代码是把字符串“zhangsan”全部字母变成大写后再显示出来:ZHANGSAN;第 6 行代码是从字符串“zhangsan”中截取从第2个位置到第3个位置的字符并组成一个新的子符串显示出来:ha
6.4.2 字符串特性
不同的程序语言,会有一些相类似的语法或元素,比如if、for、while、switch等之类的语法,也会有字符、数例、字符串之类的元素。尽管如此,这些语言处理问题的方式不尽相同,都会有一些细微的差别。对于Java中的字符串来说,就需要注意一些特性:
1. 字符串常量与字符串池
预测一下这个程序代码的输出结果是什么?
范例:StringDemo01.java
public class StringDemo01{ public static void main(String[] args){ String str1 = new String("Hello"); String str2 = new String("Hello"); System.out.println(str1 == str2); } }
可能你会觉得输出结果应该是 true ,但不是,结果是 false 。在说为什么是 false 之前,再来看下这个代码:
范例:StringDemo02.java
public class StringDemo02{ public static void main(String[] args){ String str1 = "Hello"; String str2 = "Hello"; System.out.println(str1 == str2); } }
本例的输出结果为 true 。
为什么会发生这种情况呢?接下来看下这两个范例的内存结构图。
第一个范例结构图如下:
第二个范例结构图如下:
分析:从上面的结构图不难看出,当使用String str1 = new String("Hello");的方式定义字符串类型变量str1时,内存中会分配两个堆内存地址。str1首先会指向值为"Hello"的这块区域,但执行new操作后,str1的指向就发生了变化,指向到了另一个"Hello"区域,从面导致堆内存中产生了一个垃圾区域。由于str1和str2指向的不是同一区堆内存区域,所以它们的指向的内存地址值是不一样的,所以比较的结果是 false 。而使用String str1 = "Hello";这种方式后,它们都是指向的"Hello"这块区域,所以结果是 true 。根据这个结果也不难看出,在定义字符串变量时,尽量使用第二个范例方式来定义。这种方式好处之一就是前面提到的减少了垃圾内存的产生,另一个好处就是可以使用字符串池。
字符串池是一种形象的比喻,它是内存中的一区特殊区域。当使用String str1 = "Hello";的方式来定义时,虚拟机(JVM)首先会在内存的字符串池里去查找有没有值为"Hello"的字符串存在,如果存在就直接把变量str1指向字符串"Hello"所对应的地址;如果不存在就创建一个新的字符串"Hello"并把变量str1指向它,同时把这个字符串放入字符串池中,以方便下次直接使用。
如果现在想让范例SringDemo01.java中输出true,又该怎么办呢?把程序代码修改如下:
范例:StringDemo01.java
public class StringDemo01{ public static void main(String[] args){ String str1 = new String("Hello"); String str2 = new String("Hello"); //System.out.println(str1 == str2); System.out.println(str1.equals(str2)); } }
通过这样的修改,此时输出结果就变为了true。
那么使用equals()方法和“==”进行比较的区别是什么呢?
对于基本数据类型中的整型来说,使用“==”比较的既是值,也是地址。因为整型的地址和值是一样的;而对于引用对象来说,“==”比较的是引用变量的地址,equals()方法比较的是引用变量的值。
2. 不可变字符串
在Java中,字符串对象一旦创建,就无法更改对象的内容。字符串对象没有提供任何方法可以来改变字符串的内容。来看看下面的程序代码。
范例:StringDemo03.java
public class StringDemo03{ public static void main(String[] args){ String str1 = "Hello"; String str2 = str1 + " World"; System.out.println(str); } }
本例输出结果是:Hello World
这个结果貌似和想象的结果是一样的,那么,来看下内存分配结构图,你会发现它们的执行过程。
分析:从上图中可以看出,当执行String str2 = str1时,字符串变量str2指向到了字符串变量str1对应的地址,而" World"来身就是一个字符串对象,因此它在内存中也会分配空间。当执行String str2 = str1 + " World";时,字符串变量str2就与之前所指向的内存地址段开,重新指向到了一个新的字符串"Hello World"对应的内存地址。
从整个分析过程不难看出,字符串从定义好后,就不会发生变化,之所以输出的结果和分析的结果是一样的,但是它们并不是做的累加操作。
对于“+”运算符来说,如果用于基本数据类型中整型操作,是作加法运算;如果用于字符串操作,就是把两个字符串连接起来,产生一个新的字符串。
从上例可以看出,使用字符串拼接来操作字符串效率是不高的,如果用于循环中,效率会更差。因为会不断的产生新的字符串,而旧的字符串就变为了垃圾对象。为了提高字符串的执行效率,Java提供了StringBuilder类。
范例:StringBuilderDemo.java
public class StringBuilderDemo{ public static void main(String[] args){ StringBuilder build = new StringBuilder(); for(int i=1; i<=1000; i++){ build.append(i).append('+'); } System.out.prinln(build.toString()); } }
StringBuilder是JDK5之后新增的类(java.lang.StringBuilder)。这个是是线性(多线程,后面再介绍)不安全的,而在这个类出现之前,JDK也提供了StringBuffer类,它是线性安全的。StringBuilder每次append()后,都会返回原对象,方便进行下次操作。并且在整个过程中只产生一个StringBuilder对象。
思考一下接下来的程序代码输出结果是什么?
范例:StringDemo04.java
public class StringDemo04{ public static void main(String[] args){ String str1 = "jo" + "ck"; String str2 = "jock"; System.out.println(str1 == str2); } }
程序最终输出结果为:true
为什么呢,大家思考一下,然后参考上面的内存分配结构图就明白了。
6.5 内部类
在一个类中包含有另一个类,则被包含的类称为内部类(Inner class),包含的类称为外部类(Outer class)。
内部类可以定义在类区域块中,下面程序代码段定义了非静态的内部类:
范例:Outer.java
public class Outer{ class Inner{ } }
内部类访问特点:
1. 内部类可以直接访问外部类的成员;
2. 外部类要访问内部类,必须建立内部类的实例化对象。
了解了内部类的访问特点后,来看看开发中如何使用内部类。
范例:InnerClassDemo01.java
class Outer{ private int num = 20; //定义内部类 class Inner{ void show(){ System.out.println("内部类..." + num);//访问外部类成员 } } //访问内部类 public void method(){ Inner in = new Inner(); in.show(); } } public class InnerClassDemo01{ public static void main(String[] args){ Outer outer = new Outer(); outer.method(); } }
本例是通过创建外部类的实例,然后通过调用外部类的method()方法来执行内部类。在外部类的method()方法中,先实例化内部类,然后调用内部类的show()方法。
在main()方法中,除了使用上例的方式访问内部类外,还可以直接通过下面代码方式来访问内部类:
范例:InnerClassDemo02.java
public class InnerClassDemo02{ public static void main(String[] args){ Outer.Inner inner = new Outer().new Inner(); inner.show(); } }
从代码中可以看出,首先创建外部的实例,然后再通过外部类的实例来创建内部类的实例。
内部类也可以使用public、protected、private来修饰。如:
范例:Outer.java
class Outer{ private class Inner{ void show(){ System.out.println("内部类..."); } } }
内部类可以很方便的存取外部类的成员。一般来说,非静态内部类会声明为private,作用是为辅助类中某些操作而设计的,外部不用知道内部类的存在。内部类编译完后,会生成外部类的名称.class和外部类的名称$内部类的名称.class两个字节码文件。后者表示为内部类。
如果内部类中定义了静态成员,那么这个内部类必须是静态的内部类,需要使用static关键词来修饰。
范例:StaticOuter.java
class StaticOuter{ private static int num = 20; static class StaticInner{ void show(){ System.out.println("静态内部类中非静态方法..." + num); } static void say(){ System.out.println("静态内部类中静态方法..." + num); } } public void print(){ Inner in = new Inner(); in.show(); } }
本例中由于内部类中定义了静态方法say(),因此该内部类必须定义为静态的内部类。而静态内部类中访问了外部成员,所以外部类中的num成员需要定义为静态成员。
当内部类是静态时候,此时这个内部类就可以当着一个外部类来使用。调用方式如下:
范例:InnerClassDemo03.java
public class InnerClassDemo03{ public static void main(String[] args){ StaticOuter.StaticInner inner = new StaticOuter.StaticInner(); inner.show(); } }
如果静态内部类中的成员也是静态的,则可以通过如下方式调用:
范例:InnerClassDemo04.java
public class InnerClassDemo04{ public static void main(String[] args){ StaticOuter.StaticInner.say(); } }
内部类可以访问外部类成员,那么它是如何访问的呢?通过下面的程序可以了解执行方式:
范例:InnerClassDemo05.java
class Outer{ int num = 20; class Inner{ int num = 18; void out(){ int num = 16; System.out.println(num); } } public void print(){ new Inner().out(); } } public class InnerClassDemo05{ public static void main(String[] args){ new Outer().print(); } }
本例的输出结果为:16
如果要想输出18,则必须把内部类中out()方法的输出语句改为:System.out.println(this.num);才能实现该需求;如果要想输出20,则必须把内部类中out()方法的输出语句改为:System.out.println(Outer.this.num);才能实现该需求。
由此可知,内部类之所有可以访问外部类的成员,是因为它持有外部类的引用:外部类名.this。
实际开发中,内部类还可能被定义在外部类的某个方法中,例如下面代码所示:
范例:InnerClassDemo06.java
class Outer{ public void print(){ final int num = 20; class Inner{ void out(){ System.out.println(num); } } new Inner().out(); } } public class InnerClassDemo06{ public static void main(String[] args){ new Outer().print(); } }
当内部类在外部类中的某个方法内时,要想访问该方法中的局部变量时,该局部变量必须被关键词final修饰。换句话说,方法中的内部类只能访问被final修饰的局部变量。
6.6 匿名内部类
所谓匿名内部类就是没有名称的内部类,它是内部类的简写格式。它实际不上是一个匿名的子类对象。
匿名内部类的格式如下:
new 父类或者接口(){实现代码块}
范例:InnerClassDemo07.java
class Outer{ int num = 20; public void print(){ new Object(){ void out(){ System.out.println(num); } }.out(); } } public class InnerClassDemo07{ public static void main(String[] args){ new Outer().print(); } }
在Swing和Android开发中经常使用到内部类。