第4章 认识对象
学习目标
区分基本类型与对象类型
了解对象与引用的关系
从打包器认识对象
以对象观点看待数组
认识字符串的特性
4.1 类与对象
第3章讲的都是基本类型,这一章开始讲对象类型。在Java中一切皆对象(Object),要产生对象必须先写类(Class),类是对象的模版,对象是类的实例(Instance)
4.1.1 定义类
一个类就是一个模版,它包括了所有的属性和方法,就象设计师手上的设计图纸一样,如果我们需要设计一个大黄鸭,那么我们首先会绘制一个大黄鸭的设计图,上面定义了大黄鸭的大小、颜色等,我们会根据设计图制作出实际的大黄鸭,每个大黄鸭都是同一个款式,但会拥有自己的大小,颜色。
图4.1 大黄鸭设计、制作
如果我们要设计的就是一个关于大黄鸭的设计的软件,那如何使用Java来编写呢?首先就要在程序中定义类,这相当于图4.1中大黄鸭的设计图:
class BigDuke{
String color;
int size;
}
类定义时使用class关键字,名称使用BigDuck,相当于为大黄鸭的设计图取名叫BigDuck,大黄鸭的颜色用字符串表示,也就是color变量,可存储”深黄”,”浅黄”,”大黄”等。
大黄鸭的尺寸会是’大’,’中’,’小’,所以使用了char类型。如果要在程序中,利用BigDuck在程序内建立实例,就要使用new关键字。例如:
new BigDuck();
在对象术语中,这叫做实例化一个对象。如果要有个标签,和这个对象绑定,可以这样声明:
BigDuck bd;
是不是很象前面我们声明基本变量的形式?不过Java专门为这种指向对象的“变量”取了个术语,叫“引用”(Reference Name)。如果要将bd绑到新建的对象上,可以使用“=”赋值,在Java术语中,叫“将bd引用至实例化对象”。如:
BigDuck bd=new BigDuck();
图4.2 class、new、=等语法对应
提示:对象(Object)与实例(Instance)在Java中几乎是等义的名词,本书中就视为相同意义。
在BigDuck类中,定义了color与size两个变量,以Java的术语来说,叫作定义两个属性(Field)成员,或叫定义两个成员变量,这表示每个新建的BigDuck实例中都能拥有各自不同的color值与size值。(默然说话:其实简单说,就是类是一个模版,它定义的类别应该具有的属性,但没有值,而实例对象则会具备相同的属性,且每个属性可以有不同的值。比如人类可以定义肤色与身高,然后每个人类的实例可以拥有不同的肤色值与身高值。)
class BigDuck{
String color;
char size;
}
/**
* 说明类和对象关系的代码示例
* 文件:ObjectTest.java
* @author mouyo
*/
public class ObjectTest {
public static void main(String[] args) {
//实例化大黄鸭对象
BigDuck foodDuck=new BigDuck();
BigDuck lakeDuck=new BigDuck();
//为每个大黄鸭对象的属性赋值
foodDuck.color="浅黄";
foodDuck.size='小';
lakeDuck.color="深黄";
lakeDuck.size='大';
//显示每个大黄鸭对象的各自属性值
System.out.println("黄鸭饭:"+foodDuck.color+","+foodDuck.size);
System.out.println("湖中大黄鸭:"+lakeDuck.color+","+lakeDuck.size);
}
}
在这个ObjectTest.java中,定义了两个类,一个是公开(public)的ObjectTest类,Java规定,公开(public)的类名必须与文件一致。另一个是非公开的BigDuck。
提示:只要有类定义,编译程序就会产生一个.class文件。上例中会产生ObjectTest.class和BigDuck.class两个文件
程序中实例化了两个BigDuck对象,并分别声明了foodDuck与lakeDuck两个引用变量,接着指定了foodDuck绑定的对象所拥有的color和size分别赋值为”浅黄”和’小’,而lakeDuck绑定的对象所拥有的color和size分别赋值为”深黄”和’大’。最后分别显示foodDuck与lakeDuck各自拥有的属性值:
run:
黄鸭饭:浅黄,小
湖中大黄鸭:深黄,大
成功构建 (总时间: 0 秒)
执行结果如上,可以看出来,每个对象都拥有自己的数据,它们是相互独立,互不影响的。(默然说话:这也就在提醒我们,当我们进行属性赋值的时候,除了要关心属性的值之外,还要关心被赋值的属性是哪个对象,如果搞错了对象,那值也就是不对的了。)
观察为每个大黄鸭对象的属性赋值的那段代码,你会发现它们虽然是给不同的对象赋值,但代码的书写基本是一样的。只要是一样的代码,我们就可以进行模版化,让这些代码只写一遍。这里我们可以使用构造方法。构造方法是与类名同名的方法,它没有返回值。
public class BigDuck2 {
String color;
char size;
BigDuck2(String color,char size){//构造方法
this.color=color;//将参数color指定给color属性
this.size=size;
}
public static void main(String[] args) {
BigDuck2 foodDuck=new BigDuck2("浅黄",'小');//使用指定构造方法建立对象
BigDuck2 lakeDuck=new BigDuck2("深黄",'大');
//显示每个大黄鸭对象的各自属性值
System.out.println("黄鸭饭:"+foodDuck.color+","+foodDuck.size);
System.out.println("湖中大黄鸭:"+lakeDuck.color+","+lakeDuck.size);
}
}
在这个例子中,定义新建对象时,必须传入两个参数给String类型的color与char类型size参数,而构造方法中,由于color参数与color属性名相同,你不可以直接写color=color,而要写成this.color=color。
在实际使用new创建对象时,就可以直接传入字符串与字符,分别代表BigDuck实例的color与size值,执行结果与上个例子相同。
4.1.2 使用标准类
第一章已经讲过,Java Se提供了标准API,这些API就是由许多类组成,你可以直接取用这些标准类,省去自己打造基础的需求。下面举两个基本的标准类:java.util.Scanner
和java.math.BigDecimal
。
1.使用java.util.Scanner
目前为止的程序例子都很无聊,变量值都是写死的,没有办法接受用户的输入。如果要在命令行模式下取得用户输入,我们就可以使用java.util.Scanner
。
package cn.com.speakermore.ch04;
import java.util.Random;
import java.util.Scanner;//导入标准API中的Scanner类
/**
* 猜数
* @author mouyo
*/
public class Guess {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);//创建Scanner对象
int number=new Random().nextInt(10);
int guess;
do{
System.out.print("猜数(0~9)");
guess=input.nextInt();//获得用户输入的整型数
}while(guess!=number);
System.out.println("恭喜!猜对了!");
}
}
由于我们不想每次都输入java.util.Scanner
,所以一开始就使用import告诉编译器:“我需要使用java.util.Scanner
,请帮我导入。”在实例化(new)Scanner对象时,必须传入System.in
对象,它是一个java.io.InputStream
流对象(默然说话:关于这些内容,第10章会详细介绍,你现在就当做乌龟的屁股——规定——来照着写就可以了)。
接下来就可以很简单的通过Scanner的对象(名字叫input)来获得键盘的输入了。大家完全不用管它是怎么获得键盘输入的,反正就是能获得。(默然说话:这就是面向对象里面的一个概念:封装。举个生活中的例子:你只要懂得打开水龙头,就可以得到水,而完全不用去关心水怎么是从水龙头里流出来的。忽略细节,可以让我们更专心的做自己想要去完成的事情,而不用啥都操心,这就是封装。后面的相关章节还会详细阐述。)
Scanner
的nextInt()
方法会查看键盘输入的是不是数字,如果是数字就会接收。Scanner
对每个基本类型,都会有个对应的nextXxx()方法,如nextByte()、nextShort()、nextLong()、nextFloat()、nextDouble()、nextBoolean()
等,如果要取字符串,则使用next()
,如果是取一行字符串,则使用nextLine()
(以换行分隔)。
提示:包是java管理类的一种方式,后面相关内容会介绍。包名以java开头的类,都是标准API提供的类。
2.使用java.math.BigDecimal
第2章介绍基本类型的时候有个问题:1.0-0.8的结果是多少?答案肯定不是0.2,而是0.19999999999999996。为什么?java的bug?肯定不是,因为你用其他的语言来算这个算式也会得到同样的结果。
简单来说,java遵守IEEE754浮点数运算规范,使用分数与指数来表示浮点数。例如0.5是使用1/2来表示,0.75是使用1/2+1/4来表示,而0.1会使用1/16+1/32+1/256+…无限循环下去,无法精确表示。因而造成运算上的误差。
再来举个例子,你觉得下面的程序片段会显示什么结果?
double a=0.1;
double b=0.1;
double c=0.1;
if((a+b+c)==0.3){
System.out.println("等于0.3");
}else{
System.out.println("不等于0.3");
}
由于浮点数误差的关系,结果是显示“不等于0.3”。类似的例子还有很多,结论就是,如果要求精确度,那就要小心地使用浮点数,而且别用==直接比较浮点数运算结果。
那么要怎么办能得到更好的精确度?可以使用java.math.BigDecimal
类。
import java.math.BigDecimal;
/**
*DecimalDemo.java
* @author mouyo
*/
public class DecimalDemo {
public static void main(String[] args) {
BigDecimal a=new BigDecimal("1.0");
BigDecimal b=new BigDecimal("0.8");
BigDecimal result=a.subtract(b);
System.out.println(result);
System.out.println(1.0-0.8);
}
}
创建BigDecimal
的方法之一是使用字符串,BigDecimal
在创建时会分析字符串,以默认精度进行接下来的运算。BigDecimal
提供有plus()、substract()、multiply()、divide()
等方法,可以进行加、减、乘、除等运算,这些方法都会返回代表运算结果的BigDecimal
。
上面的例子可以看到0.2的结果,再来看利用BigDecimal
比较相等的例子。
import java.math.BigDecimal;
/**
*DecimalDemo2.java
* @author mouyo
*/
public class DecimalDemo2 {
public static void main(String[] args) {
BigDecimal a=new BigDecimal("0.1");
BigDecimal b=new BigDecimal("0.1");
BigDecimal c=new BigDecimal("0.1");
BigDecimal result=new BigDecimal("0.3");
if(a.add(b).add(c).equals(result)){
System.out.println("等于0.3");
}else{
System.out.println("不等于0.3");
}
}
}
由于BigDecimal
的add()
方法都会返回代表运算结果的BigDecimal
,所以就直接利用返回再次add()
。最后再调用equals()
方法比较相等。最后的结果是等于0.3。
4.1.3 对象指定与相等性
在上一例中,比较两个BigDecimal
是否相等,是使用equals()
方法而非使用==运算符,为什么?前面说过,在Java中有两大类型系统,基本类型和引用类型,基本类型把值直接放到变量中,而引用类型是把对象的内存地址放入变量中。
当使用=(默然说话:记住!这个不是等号,是赋值号,用来把一个值放到某个变量中!)给基本变量赋值时,就是把值直接放入了变量中,而引用类型变量赋值时,放入的是一个对象的所在位置的地址。正因为如此,当使用==进行比较时,基本变量就是比较了值是否相等(默然说话:因为基本变量就保存了一个值呀!)而引用类型变量比较的却是内存位置是否相等(因为引用类型变量保存的是地址呀!)。
这是基本类型变量的代码与示例图,记住,基本类型保存的是值!
int a=10;
int b=10;
int c=a;
System.out.println(a==b);
System.out.println(a==c);
图4.3 基本变量的赋值与比较相等的结果
图4.4 基本变量的赋值与比较相等的原理
这是引用类型变量的代码与示例图,记住,引用类型保存的是地址!
BigDecimal a=new BigDecimal("0.1");
BigDecimal b=new BigDecimal("0.1");
BigDecimal c=a;
System.out.println(a==b);
System.out.println(a==c);
图4.5 引用类型变量的赋值与比较相等
图4.6 引用类型变量的比较相等结果
使用==比较的就是变量里所保存的值是否相等,由于引用类型保存的值是对象的内存地址,所以比较两个引用型变量相等,就是比较两个引用变量所引用的对象是不是同一个(默然说话:内存地址相同,就是同一个变量,因为不可能在同一个内存地址中放两个不同的对象。),如果我们想要的结果是两个对象的内容是不是一样,我们需要使用equals()方法。(默然说话:上面的程序中可以看到两个对象都装着“0.1”这个数,所以如果我们是要两个对象装的内容是不是一样,是不能使用==来比较的,因为它比较的是地址)
提示:其实从内存的实际运作来看,=与==对于基本类型变量与引用类型变量的作用并没有不同,只是因为它们所保存的值的意义不一样,才造成了这一区别。
4.2 基本类型包装类
基本类型long、int、double、float、boolean等,在J2SE5.0之前必须手动使用Long、Integer、Double、Float、Boolean等打包为对象,才能当作对象来操作。5.0之后开始自动打包了。
4.2.1 包装基本类型
Java的基本类型在于效率,但更多的时候,会使用类建立实例,因为对象本身可以提供更多信息,更多方便的操作。可以使用Long、Integer、Double、Float、Boolean、Byte等类来包装(Wrap)基本类型。
Long、Integer、Double等类就是所谓的包装类(Wrapper),正如此名称所示,这些类主要目的就是提供对象实例作为“壳”,将基本类型包装在对象中,就像是将基本类型当作对象操作。
/**
* IntegerDemo.java
* @author mouyo
*/
public class IntegerDemo {
public static void main(String[] args) {
//基本类型
int data1=10;
int data2=10;
//包装类型
Integer wrapper1=new Integer(data1);
Integer wrapper2=new Integer(data2);
//基本类型除法,只能得到整数部分,小数被舍弃
System.out.println(data1/3);
//使用包装类的方法把整数转为小数,得到了有小数的结果
System.out.println(wrapper1.doubleValue()/3);
//使用包装类提供的方法进行比较,得到更多的信息
System.out.println(wrapper2.compareTo(wrapper2));
}
}
包装类都放在java.lang包中,这个包是java编译器默认会导入的包,所以不需要import语句来显示导入。包装一个基本类型就是new出包装类,然后将基本类型传给包装类就可以了。
基本数据类型如果都是整型,那最后的计算结果也会是整形。
如果我们期望得到小数的结果,那么可以使用包装类的转换方法来得到一个小数,然后计算机在运算时会自动类型转换为小数后再做计算,最后的结果就也是小数(默然说话:其实我们平时写程序时,是直接把整型数写成小数。比如10/3.0,这样的结果就会是小数了。)。
Integer提供compareTo()方法,可与另一个Integer对象比较,如果相同就返回0,如果小于传入的对象就返回-1。否则就是1。而==和!=只能得到很少的信息。
4.2.2 自动装箱、拆箱
除了使用new来包装之外,从J2SE5.0之后提供了自动装箱功能。可以这样包装基本类型:
Integer wrapper=10;
编译程序会自动判断是否能进行自动装箱,在上例如你的wrapper会被赋值为一个Integer对象。其他基本类型也是一样的。改写下上面的代码:
Integer data1=10;
Integer data2=20;
System.out.println(data1.doubleValue() / 3);
System.out.println(data1.compareTo(data2));
程序看上去简洁很多,data1和data2在运行时会自动装箱Integer对象。自动装箱还可以这样用:
int i=0;
Integer wrapper=i;
也可以使用更一般化的Number来自动装箱:
Number num=3.11f;
3.11f会先被自动装箱为Float,然后引用给num。
Java SE 5.0开始,还可以进行自动拆箱,也就是把包装类自动取值赋给基本类型变量。如:
Integer wrapper=10;//自动装箱
int a=wrapper;//自动拆箱
在运算时,也可以进行自动装箱与拆箱,如:
Integer a=10;
System.out.println(i+10);
System.out.println(i++);
4.2.3 自动装箱、拆箱的内幕
所谓自动装箱与拆箱其实只是编译器帮我们做了本来是我们需要自己做的事情。编译器在编译时会根据我们所写的代码来决定是否进行装箱或拆箱。例如下面的代码:
Integer number=100;
在Oracle的JDK中 ,编译程序会自动将代码展开为:
Integer localInteger=Integer.valueOf(100);
但自动装拆箱其实是有问题的,例如:
Integer a=null;
int j=i;
这段代码在编译的时候是不会报错的,因为null是一个特殊对象,它可以指定给任何声明的对象,表示没有引用到任何的内存地址,但是一旦执行编译出的代码,就会报错,因为编译后的代码是这样的:
Object localObject=null;
int i=localObject.intValue();
这里由于localObject对象被赋值为空,而代表没有任何的内存位置,所以也就不可能执行第二句调用intValue()方法,此时计算机就会报错NullPointerException
(空指针异常,有的教科书或老师又把它叫做空点异常)。表示你想调用一段根本不在内存里存在的代码。
除了有这方面的bug之外,还有一些稀奇古怪的现象。比如下面的代码:
/**
* Boxing.java
* @author mouyo
*/
public class Boxing {
public static void main(String[] args) {
Integer i1=100;
Integer i2=100;
if(i1==i2){
System.out.println("相等");
}else{
System.out.println("不相等");
}
}
}
这个代码没问题,结果是相等。
图4.7 结果相等
可是,我们只要改一下,象下面这样:
package cn.com.speakermore.ch04;
/**
* Boxing.java
* @author mouyo
*/
public class Boxing {
public static void main(String[] args) {
Integer i1=200;
Integer i2=200;
if(i1==i2){
System.out.println("相等");
}else{
System.out.println("不相等");
}
}
}
能看出来改了哪里么?对,只是把100换成了200,运行之后的结果却变成了不相等。
图4.8 200之后的结果是不相等。
为何是这样的结果,这是因为在Integer的自动装箱过程中,它使用了valueOf()方法,而valueOf()方法会建立一个缓存,缓存那些小于128的整数。这样,当我们的值小于128时,值相同,对象就是相同的,但是一旦大于等于128,由于没有缓存,虽然值是相同的,但对象就会不一样。用==比较时,自然就得到了不相等的结果。
所以,结论还是前面已经讨论过的,别使用==或!=来比较两个对象的值是否相同(因为引用型数据类型都是比较的地址),而要使用equals()方法。
4.3 数组对象
数组在Java中就是对象,所以前面介绍过的对象基本性质,在操作数组时也都要注意,如引用名的声明、=指定的作用、==与!=的比较等。
4.3.1 数组基础
数组基本上是用来收集数据,具有下标(index)的数据结构,在Java中要声明数组并初始值,可以如下:
int[] score={88,87,99,67,77,81,86,55,46,78};
这段代码创建一个数组,因为使用int[]声明,所以会在内存中分配长度为10的int连续空间,每个空间依次存储了大括号中的整数,每个整数都用一个下标来标识,下标从0开始,所以10个长度的数组,下标最大就只到9。如果你使用了10或以上的数字作下标,就会抛出ArrayIndexOutOfBoundException(数组下标越界异常)。
数组使用下标来获得每一个数据,重点就是可以很方便的和循环结合,用很少的代码对大量数据进行批量处理:
/**
* Score.java
* @author mouyo
*/
public class Score {
public static void main(String[] args) {
int[] score={88,87,99,67,77,81,86,55,46,78};
for (int i = 0; i < score.length; i++) {
System.out.println("学生分数:"+score[i]);
}
}
}
在声明的数组名称旁加上[]并指定下标,就可以取得对应值,上例从i为0-9,逐一取出值并显示出来。
图4.9 10个学生分数的显示
在Java中数组是对象,而不是单纯的数据集合,数组的length属性可以取得数组长度。也就是数组的元素个数。
其实上面这个程序可以使用更简单的方式来编写,因为是顺序取到数组中每一个值,所以我们可以使用增强式for循环,这是从JDK5开始出现的更方便的for循环,我基本是强烈推荐,在需要遍历一个数组的时候都使用增强式for循环。
for (int score:scores ) {
System.out.println("学生分数:"+score);
}
这个程序片段会取得scores数组第一个元素,指定给score变量后执行循环体,接着取得scores中第二个元素,指定给score变量后执行循环体。依此类推,直到scores数组中所有元素都访问完为止。将这段for循环片段取代Score类中的for循环,执行结果相同。实际上,增强式for循环也是Java提供的方便功能。
如果要给数组的某个元素赋值,也是要通过下标。例如:
scores[3]=86;
System.out.println(scores[3]);
上面这个程序片段将数组中第4个元素(因为下标从0开始,下标3就是第4个元素)指定86,所以会显示86,所以会显示86的结果。
一维数组使用一个下标存取数组元素,你也可以声明二维数组,二维数组使用两个下标存取数组元素。例如,声明数组来储存XY坐标位置要放的值。
/**
* XY.java
* @author mouyo
*/
public class XY {
public static void main(String[] args) {
int[][] cords={
{1,2,3},
{4,5,6}
};//声明二维数组并赋值初始值
for(int x=0;x<cords.length;x++){//获得有几行
for(int y=0;y<cords[x].length;y++){//获得每行有几个元素
System.out.print(cords[x][y]+" ");
}
System.out.println("");
}
}
}
要声明二维数组,就是在类型关键词旁加上[][]。初学者暂时将二维数组看作是一个表格会比较容易理解,由于有两个维度,所以首先得通过cords.length得到有几行,然后再使用cords[x].length获得每行有几个元素,之后再一个一个进行输出。
二维数组有两个下标,所以也要使用二重嵌套循环来完成取值遍历的代码,同样由于这里对下标并没有特别需要,所以也可以使用增强for循环来完成
for(int[] row:cords){//获得有几行
for(int value:row){//获得每行几个元素
System.out.print(value+" ");
}
System.out.println("");
}
最后执行结果相同。
图4.10 二维数组执行结果
提示:如果是三维数组,就是在类型关键字后使用三个[],如果是四维就是四个[]。依此类推。不过基本上是用不到如此复杂的维度,现代编程通常一维就足够了,二维都基本上不用的。
4.3.2 操作数组对象
前面都是知道元素值来建立数组的例子,如果事先不知道元素值,只知道元素个数,那可以使用new关键字指定长度的方式创建数组。例如:
int[] scores=new int[5];
数据类型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0 |
char | \u0000(即’’) |
boolean | false |
类 | null |
在Java中只要看到new,就一定是创建对象(默然说话:计算机的实际操作是划分一块内存空间供对象使用),这个语法其实已经说明数组就是一个对象。使用new创建数组之后,数组中每个元素都会被初始化,如表4.1所示:
表4.1 数组元素初始值
数据类型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0 |
char | \u0000(即’’) |
boolean | false |
类 | null |
如果默认值不符合你的需求,可以使用java.util.Arrays的fill()方法来设定新建数组的元素值。例如,每个学生的成绩默认60分:
import java.util.Arrays;
/**
* Score2.java
* @author mouyong
*/
public class Score2 {
public static void main(String[] args) {
int[] scores=new int[10];
System.out.println("默认初始值");
for(int score:scores){
System.out.print(score+" ");
}
System.out.println("\n初始填充后");
Arrays.fill(scores, 60);
for(int score:scores){
System.out.print(score+" ");
}
}
}
执行结果如下:
图4.11学生分数数组的初始化值
数组既然是对象,再加上我们讲过,对象是根据类而建立的实例,那么代表数组的类定义在哪里呢?答案是由JVM动态产生。你可以将int[]看作是类名称,这样,根据int[]声明的变量就是一个引用变量,那么下面这段代码会是什么结果?
int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=scores1;
scores2[0]=99;
System.out.println(scores1[0]);
因为数组是对象,而scores1与scores2是引用名称,所以将scores1赋值给scores2的意思就是将scores1引用的对象内存地址赋值给scores2,所以两个变量指向了同一块内存,那么,当你通过scores2[0]对这个数组的第1个元素进行修改之后,scores1也能看到变化(默然说话:记住,因为它们的指向是同一个地址,再想想前面给大家的引用变量声明的动画)。
所以最后的输出scores1[0]的值也是99。
再来看前面提过的二维数组:
int[][] cords=new int[2][3];
这个语法其实是建立了一个int[][]类型的对象,里面有2个int[]类型的对象。这2个一维的数组对象的长度都是3。初始值为0。
图4.12二维数组的对象引用示意
对,从上图中我们可以得到一个结论,其实Java的二维数组根本不是一个表格,所以Java完全可以建立一个不规则的数组。例如:
/**
* IrregularArray.java
* @author mouyo
*/
public class IrregularArray {
public static void main(String[] args) {
int[][] arr=new int[2][];//声明arr对象为二维数组
arr[0]=new int[]{1,2,3,4,5};//arr[0]是一个长度为5的一维数组
arr[1]=new int[]{1,2,3};//arr[1]是一个长度为3的一维数组
//从输出可以看出来,第一个输出了5个数,第二个输出了3个数,并非一个表格
for(int[] row:arr){
for(int value:row){
System.out.print(value+" ");
}
System.out.println();
}
}
}
这个例子我们可以看到,在new一个二维数组时可以只写最高维,而不用写最后一个维度的数量。然后再给每个数组下标实例化不同的低维度数组。而具体的数值就放在最低维度指向的空间里。(默然说话:我惊讶的发现,当写了二维声明之后,不能再使用直接初始化的语法了,也就是说,不能写成arr[0]={1,2,3,4,5},必须写成arr[0]=new int[]{1,2,3,4,5},可能是因为一开始声明时没有给出低维度的长度,所以必须重新对低维度划分空间罢)。
当然,其实这个初始化的代码也可以写为:
int[][] arr={
{1,2,3,4,5},
{1,2,3}
};
以上都是基本类型创建的数组,那么引用类型创建的数组会是什么样的情况呢?首先看看如何用new关键字建立Integer数组:
Integer[] scores=new Integer[3];
看上去不过就是把int换成了类Integer而已。那想想这个数组创建了几个Integer对象呢?错误!不是3个,而是一个Integer对象都没有创建。回头看表4.1,对象类型的默认值是null。这里只创建出一个Integer[]对象,它可以容纳3个Integer对象,但是因为对象型的默认初始化值为null,所以现在这三个对象都还没有产生。我们必须在给它们的每个下标都赋值时,才会有Integer对象产生。
/**
* IntegerArray.java
* @author mouyong
*/
public class IntegerArray {
public static void main(String[] args) {
Integer[] scores=new Integer[3];
System.out.println("仅实例化了数组之后,数组中的初始化值:");
for(Integer score:scores){
System.out.println(score);
}
scores[0]=new Integer(98);
scores[1]=new Integer(99);
scores[2]=new Integer(87);
System.out.println("实例化每个元素之后,数组中的元素值:");
for(Integer score:scores){
System.out.println(score);
}
}
}
代码的执行结果如下:
4.13对象类型的数组初始化
所以每次讲对象类型数组,我总是说,对象类型数组需要new两次,一次是new出对象数组,另一次是new出一系列放在数组中的对象。
上面的代码之所以这样写,主要是为了突出对象的特点,其实JDK5.0之后有了自动装箱和拆箱,所以数组元素赋值可以写成这样:
scores[0]=98;
scores[1]=99;
scores[2]=87;
对象类型其实也可以使用大括号进行初始化,只是写得要长些:
Integer[] scores={new Integer(98),new Integer(99),new Integer(87)};
同样,上面的写法也是为了强调对象型需要先new,JDK5.0之后有了自动装箱和拆箱,所以也可以写成这样:
Integer[] scores={99,98,87};
4.3.3 数组复制
在知道了数组其实是一种对象,你就要知道,下面的代码不是复制:
int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=scores1;
复制应该是出现两个内容相同的对象,但这只是让两个变量指向了同一个对象,也就是对象只有一个,并没有两个。所以真正的复制应该是这样的。
int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=new int[scores1.length];
for(int i=0;i<scores1.length;i++){
scores2[i]=scores1[i];
}
这是自己使用循环来完成每一个值的复制,其实可以使用System.arraycopy()
方法来直接完成这个过程,比我们用循环来得快:
int[] scores1={88,66,48,99,12,45,55,76};
int[] scores2=new int[scores1.length];
System.arraycopy(scores1,0,scores2,0,scores1.length);
System.arraycopy()有五个参数,分别是源数组,源数组开始下标,目标数组,目标数组开始下标,复制长度。如果使用JDK6以上,还有个更方便的Arrays.copyOf()
方法,你不用另行建立数组,Arrays.copyOf()
会帮你创建:
import java.util.Arrays;
/**
* CopyArray.java
* @author mouyong
*/
public class CopyArray {
public static void main(String[] args) {
int[] scores1={88,81,75,68,79,95,93};
int[] scores2=Arrays.copyOf(scores1, scores1.length);
System.out.println("scores2数组的元素:");
for(int score:scores2){
System.out.print(score+" ");
}
System.out.println("");
scores2[0]=99;
System.out.println("修改了scores2第1个元素之后,scores1的元素仍然不变");
for(int score:scores1){
System.out.print(score+" ");
}
}
}
执行结果如下所示:
图4.14Arrays.copyOf()进行数组复制无需自行创建数组
Java中,数组一旦创建,长度就是固定的,如果长度不够,就只能创建新的数组,将原来的内容复制到新数组中。如:
int[] scores1={88,81,75,68,79,95,93};
int[] scores2=Arrays.copyOf(scores1, scores1.length*2);
System.out.println("scores2数组的元素:");
for(int score:scores2){
System.out.print(score+" ");
}
Arrays.copyOf()
的第二个参数就是指定新数组的长度。上面程序让新创建的数组长度是原来数组长度的2倍,输出时会看到增加部分元素的值均为默认值0。
无论是System.arraycopy()还是Arrays.copyOf(),用在类类型声明的数值时,都是执行浅层复制(默然说话:也就是说,如果你的数组元素是对象,那么复制一个新的数组时,新数组还在引用原数组的对象,并不会把原数组的对象进行复制!)。如果真的要连同对象一同复制,你得自行操作,因为基本上只有自己才知道,每个对象复制时,有哪些属性必须复制。
4.4 字符串对象
在Java中,字符串本质是打包字符数组的对象。它同样也有属性和方法。不过在Java中基于效率的考虑,给予字符串某些特别且必须注意的性质。
4.4.1 字符串基础
由字符组成的文字符号称为字符串。例如,“Hello”字符串就是由’H’、’e’、’l’、’l’、’o’、五个字符组成,在某些程序语言中,字符串是以字符数组的方式存在,然而在Java中,字符串是java.lang.String实例,用来打包字符数组。所有用””包括的一串字符都是字符串。
String name="sophie";//创建String对象
System.out.println(name);//输出sophie
System.out.println(name.length());//显示长度为6
System.out.println(name.charAt(0));//显示第1个字符s
System.out.println(name.toUpperCase());//显示全大写SOPHIE
因为字符串在Java中是对象,所以它有很多的方法,上面的示例给大家看到了几个很常用的字符串方法,length()可以获得字符串长度;charAt()可以获得指定下标的字符,toUpperCase()可以把所有小写字母转成大写字母。
如果已有一个char[]数组,也可以使用new来创建String对象。如:
char[] ch={'H','e','l','l','o'};
String str=new String(ch);
也可以使用toCharArray()
方法,将字符串以char[]的形式返回。
char[] ch2=str.toCharArray()
Java中可以使用+运算来连接字符串。前面我们一直在用。
为了认识数组与字符串,可以看看程序入口main()方法的String[] args参数,在启动JVM并指定执行类时,可以一并指定命令行参数。
/**
* Avarage.java
* @author mouyong
*/
public class Avarage {
public static void main(String[] args) {
long sum=0;
for(String arg:args){
sum+=Long.parseLong(arg);//把字符串转化为长整型
}
System.out.println("平均:"+(float)sum/args.length);
}
}
在NetBeans中如果要提供命令行参数,可以这样进行操作。
-
(1)在项目上右击,在弹出的快捷菜单中选择“属性”,打开“项目属性”对话框,在左边的“类别”列表中选择“运行”。
图4.15配置命令行参数
-
(2)单击右侧上方“配置”列表框旁边的“新建…”按钮,打开“创建新的配置”对话框,在“类别”文本框中填入配置的名称,我填的是“命令行参数”。
图4.16创建新的配置
- (3)单击“主类”文本框右侧的“浏览…”按钮,从中选择你写的示例。
- (4)在“参数”文本框中手工输入参数1 2 3 4,注意,每个数字之间要用空格分开,不然JVM会认为你只输入了一个参数。
这样设定之后,点击工具栏上的“运行”按钮,就会按你所设置的配置调用程序了。
4.4.2 字符串特性
各程序语言会有细微、重要且不容忽视的特性。在Java的字符串来说,就有一些必须注意的特性:
字符串常量和字符串池。
“不可变”(Immutable)的字符串。
*1.字符串常量与字符串池
来看下面的片段:
String name1=new String("Hello");
String name2=new String("Hello");
System.out.println(name1==name2);
希望现在的你会很自信的回答:结果是false。是的,结果的确是false。因为name1和name2引用了不同的对象,尽管它们的内容一样,但内存地址不同,所以==的比较结果就是false。
图4.17片段执行结果
再看下面的片段:
String name1="Hello";
String name2="Hello";
System.out.println(name1==name2);
这个代码和上面似乎没啥区别嘛,只不过没有显式new而已。但是如果你运行一下,你就会惊讶的发现:结果是true!!!
图4.18片段执行结果
难道这里使用了不同的规则?答案是:没有,和前面的显式new的规则是一致的!所以正确的推论是:name1和name2都指向了同一个对象!
在Java中为了效率的考虑,以””包括的字符串,只要内容相同(序列、大小写相同),无论在程序中出现几次,JVM都只会建立一个String对象,并在字符串池中维护。在上面这个程序片段的第一行,JVM会建立一个String实例放在字符串池中,并把地址赋值给name1,而第二行则是让name2直接引用了字符串池中的同一个String对象。
用””写下的字符串称为字符串常量(String Literal),既然你用”Hello”写死了字符串内容,基于节省内存的考虑,自然就不用为这些字符串常量分别建立String实例。
前面一直强调,如果想比较对象的内容是否相同,不要使用==,要使用equals()。这个同样适用String。
2.不可变动字符串
在Java中,字符串对象一旦建立,就无法更改对象中任何内容,对象没有提供任何方法可以更改字符串内容。那么+连字符串是怎么做到的呢?
String name1=”Java”;
String name2=name1+”World”;
System.out.println(name2);
上面这个程序片段会显示JavaWorld,由于无法更改字符串对象内容,所以绝不是在name1引用的字符串对象之后附加World。而创建了java.lang.StringBuilder
对象,使用其append()方法来进行+左右两边字符串附加,最后再转换为toString()
返回。
简单地说,使用+连接字符串会产生新的String对象,这并不是告诉你,不要使用+连接字符串,毕竟这种做法非常方便。但是不要将+用在重复性的连接场合,比如循环或递归时。这会因为频繁产生新对象,造成效能上的负担。
比如,如果我们想输出一个从1加到100的算式(注意,不是1到100相加的结果,而是显示描述1加到100的这个算式:1+2+3+4+5……+100),你会怎么写?
也许是这样写?
for(int i=1;i<100;i++){
System.out.print(i+"+");
}
System.out.println(100);
这样写代码很简洁,但是一个一个循环输出效能是很低的。有没有更好的办法?
String str="";
for(int i=1;i<100;i++){
str+=i+"+";
}
System.out.println(str+100);
只有一个输出语句,效能大大提高!但是使用了连接字符串,会造成频繁产生新对象的问题。象上面这样的情况下,强烈建议使用StringBuilder!
/**
* OneTo100.java
* @author mouyong
*/
public class OneTo100 {
public static void main(String[] args) {
StringBuilder builder=new StringBuilder();
for(int i=1;i<100;i++){
builder.append(i).append("+");
}
System.out.println(builder.append(100).toString());
}
}
StringBuilder每次append()调用之后,都会返回原StringBuilder对象,所以我们可以连接调用append()方法。这个程序只产生一个StringBuilder对象,只进行一次输出,无论是从计算效能还是内存使用效能上来说,都是非常棒的!
提示:java.lang.StringBuilder是JDK5之后新增的类,在该版本之前,我们有java.lang.StringBuffer,StringBuilder和StringBuffer其实是一样的。但是StringBuilder的效率更高,因为他不考虑多线程情况下的同步问题。所以如果你的程序会涉及到多线程的情况,可以直接改用StringBuffer
4.4.3 字符串编码
你我都是中国人(默然说话:对的!下面要谈论的问题与中文有关,如果你就不使用中国字,那可以略过),在Java中你必然要处理中文,所以你要了解Java如何处理中文。
要知道,java源代码文档在NetBeans中是UTF-8编码。在windows简体中文版下默认是GB2312。在Eclipse中情况又会不同,Eclipse会自动让java源代码文件的编码是所在操作系统的默认编码。所以,如果你的Eclipse在Windows简体中文下就是GB2312,如果是在Mac下,就会是UTF-8。
如果你使用的是GB2312编码,在编译器编译你的代码时,会自动把所有的中文都转为Unicode编码。比如”哈啰”就会变成”\u54C8\u56C9”。这个”\uxxxx”就是Unicode的编码形式。
那么编译器是如何知道要将中国字转成哪种Unicode编码呢?当你使用javac指令没有指定-encodding参数时,会使用操作系统默认编码,如果你的源代码文件采用了不同的编码形式,则必须指定-encoding参数。
提示:在Windows下不要使用纯文本编辑器转存UTF-8文件来编写源代码,因为记事本会在文档头加BOM,这样会造成Java编译器无法编译。建议使用NotePad++。
IDE也是允许你自定义编码的,如果是NetBeans,可以在项目上右击,在弹出的快捷菜单中选择“属性”,打开“项目属性”对话框,在左边“类别”列表中选择“源”。然后在右侧最下方的“编码”选择框中选择代码的编码。
4.5 查询Java API文件
书上提到的各种Java类是如何知道应该调用什么方法呢?是通过查询Java API文档知道的。
http://docs.oracle.com/javase/8/docs/api/
这个链接就是Java API文档,里面包括了所有Java类的方法属性及使用介绍、特点介绍。
不过在平时如果知道一个Java类,想知道怎么用,我更习惯用下面这个链接。
http://www.baidu.com/
以前喜欢用谷歌,现在用不了,只好将就着用了。
4.6 重点复习
要产生对象必须先定义类,类是对象的模板,对象是类的实例。定义类使用class关键字,实例化对象使用new关键字。
想在建立对象时,一并进行某个初始流程,可以定义构造函数,构造函数是与类名同名的方法,它不能写返回值。参数根据需要指定,也可以没有参数。
Java遵守IEEE 754浮点运算规范,使用分数与指数来表示浮点数。如果要求精确度,那就要小心使用浮点数。不要使用==直接比较浮点数运算结果。
要让基本类型象对象一样的操作,可以使用Long、Integer、Double、Float等类来打包基本类型。JDK5之后提供了自动装箱和拆箱,妈妈再也不担心我不会装箱和拆箱了。
数组在Java中就是对象,下标从0开始,下标超出范围会抛异常。
无论是System.arraycopy()还是Arrays.copyOf(),用在类的数组时,都是执行浅层复制。
字符串也是对象。
字符串对象一旦建立,无法更改对象内容。不要将+用在重复性的连接场合。
使用javac指令没有指定-encoding参数时,会使用操作系统默认编码。
4.7 课后练习
4.7.1 选择题
- 如果有以下的程序代码:
int x=100;
int y=100;
Integer wx=x;
Integer wy=y;
System.out.println(x==y);
System.out.println(wx==wy);
在JDK 5以上的环境编译与执行,则显示结果是()
A.true、true
B.true、false
C.false、true
D.编译失败
- 如果有以下的程序代码:
int x=200;
int y=200;
Integer wx=x;
Integer wy=y;
System.out.println(x==y);
System.out.println(wx==wy);
在JDK 5以上的环境编译与执行,则显示结果是()
A.true、true
B.true、false
C.false、true
D.编译失败
- 如果有以下的程序代码:
int x=300;
int y=300;
Integer wx=x;
Integer wy=y;
System.out.println(wx.equals(x));
System.out.println(wy.equals(y));
在JDK 5以上的环境编译与执行,则显示结果是()
A.true、true
B.true、false
C.false、true
D.编译失败
- 如果有以下的程序代码:
int[] arr1={1,2,3};
int[] arr2=arr1;
arr2[1]=20;
System.out.println(arr1[1]);
在JDK 5以上的环境编译与执行,则显示结果是()
A.执行时显示2
B.执行时显示20
C.执行时出现ArrayIndexOfBuondException
D.编译失败
- 如果有以下的程序代码:
int[] arr1={1,2,3};
int[] arr2=new int[arr1.length]
arr2=arr1;
for(int value:arr2){
System.out.print(value);
}
在JDK 5以上的环境编译与执行,则显示结果是()
A.执行时显示123
B.执行时显示12300
C.执行时出现ArrayIndexOfBuondException
D.编译失败
- 如果有以下的程序代码:
String[] strs=new String[5]
以下描述正确的是()
A.产生5个String对象
B.产生1个String对象
C.产生0个String对象
D.编译失败
如果有以下的程序代码:
String[] strs={“java”, “java”, “java”, “java”, “java”}
以下描述正确的是()
A.产生5个String对象 B.产生1个String对象
C.产生0个String对象 D.编译失败如果有以下的程序代码:
String[][] strs=new String[2][5]
以下描述正确的是()
A.产生10个String对象
B.产生2个String对象
C.产生0个String对象
D.编译失败
- 如果有以下的程序代码:
String[][] strs={
{“java”, “java”, “java”},
{“java”, “java”, “java”, “java”}
};
System.out.println(strs.length);
System.out.println(strs[0].length);
System.out.println(strs[1].length);
以下描述正确的是()
A.显示2、3、4
B.显示2、0、1
C.显示1、2、3
D.编译失败
- 如果有以下的程序代码:
String[][] strs={
{“java”, “java”, “java”},
{“java”, “java”, “java”, “java”}
};
for(row:strs){
for(value:row){
…
}
}
空白处应该分别填上()
A.String、String
B.String、String[]
C.String[]、String
D.String[]、String[]
4.7.2 操作题
1.Fibonacci为13 欧洲数学家,在他的著作中提过,若一只兔子每月生一只小兔子,一个月后小兔子也开始生产。起初只有一只小兔子,一个月后有两只兔子,两个月后有三只兔子,三个月后有五只 …,也就是每个月兔子总数会是1、1、2、3、5、8、13、21、34、89,这就是费氏数列,可用公式定义如下:
fn=fn-1+fn-2 if n>1
fn=n if n=0,1
编写程序,可以让用户输入想计算的费氏数列的个数,由程序全部显示出来。
2.请编写一个简单的洗 程序,可以在文本模式下显示洗牌结果。
3.下面是一个数组,请使用程序使其中元素排序为由小到大:
int[] number={70,80,31,37,10,1,48,60,33,80};
4.下面是一个排序后的数组,请编写程序可让用户在数组中寻找指定数字,找到就显示索引值,找不到就显示-1:
int number={1,10,31,33,37,48,60,70,80};