类与对象

Java中有基本类型与类类型两个类型系统。

  • 基本类型
  • 类类型。

使用Java写的程序几乎都在使用对象(Object),要产生对象必须先定义类(Class).

类是对象的设计图,对象是类的实例(Instance)。

定义类

先看一个例子。 如何制作一个T恤。

  • 首先,我们需要有一个设置图

  • 然后,通过定义不同的样式, 比如颜色与尺寸

  • 制作衣服,贴上名牌。

假如我们想用Java实现这一过程,我们应该怎么做呢?

我们首先应该定义一个类,相当于上图中衣服的设计图。

class Clothes {
  String color; // 颜色
  char size; // 尺寸
}

我们来分析一下这个类的定义。

  • 类定义时使用class关键词,名称使用Clothes,相当于为衣服设计图取名为Clothes。
  • 衣服的颜色用字符串表示,也就是color变量,可储存"red"、"black"、"blue"等值。
  • 衣服的尺寸会是'S'、'M'、'L',所以用char类型声明变量。

创建类

如果要在程序中,利用Clothes类作为设计图,建立衣服实例,需要使用new关键词

new Clothes();

在Java的术语中,这叫做新建一个对象。

如果要有个名牌,专门绑定到这个对象上,可以这样声明:

Clothes c1

在Java的术语中,这叫声明引用类型变量。

如果要将c1绑到新建的对象上,可以使用“=”指定,

以Java的术语来说,称为将c1引用新建的对象。

Clothes c1 = new Clothes();

上面讲到的就如下图

object_class

.

在Clothes类中,定义了color与size两个变量,以Java术语来说,叫做定义两个成员变量。

这表示每次新建的Clothes对象,都拥有个别color与size值。

例如:

class Clothes {
    String color; // 颜色
    char size; // 尺寸
}

public class Filed {
    public static void main(String[] args) {
        Clothes c1 = new Clothes();
        Clothes c2 = new Clothes();
       c1.color = "red";
       c1.size = 'S';
       c2.color = "green";
       c2.size = 'M';

       System.out.printf("c1 (%s %c)%n", c1.color,  c1.size);
       System.out.printf("c2 (%s %c)%n", c2.color,  c2.size);
    }
}

从上面的代码我们可以看出,每次创建一个新的对象(c1和c2),我们都需分别定义为每个对象的属性去设置对应的值。其实我们有更好的方式去定义属性。

构造方法

构造函数是与类名称同名的方法(Method),我们可以用来做一些对象初始化的操作。

我们来看一下代码。

class Clothes2 {
    String color; // 颜色
    char size; // 尺寸
    
    // 构造方法
    public Clothes2(String color, char size) {
    	this.color = color;
    	this.size = size;
    }
    
}

public class Filed2 {
    public static void main(String[] args) {
        Clothes2 c1 = new Clothes2("red", 'S');
        Clothes2 c2 = new Clothes2("green", 'M');

        System.out.printf("c1 (%s %c)%n", c1.color, c1.size);
        System.out.printf("c2 (%s %c)%n", c2.color, c2.size);
    }
}

使用标准类

Java SE提供了标准API,这些API就是由许多类所组成的,你可以直接取用这些标准类,省去写程序时重新打造基础的需求。

下面来举两个基本的标准类:java.util.Scanner与java.math.BigDecimal。

使用java.util.Scanner

一个简单的文本扫描器,可以使用正则表达式解析原始类型和字符串。

		Scanner sc = new Scanner(System.in);
		System.out.println("请输入一个整数:");
		int i = sc.nextInt();
		System.out.println(i);
		sc.close();

API地址

中文

http://www.matools.com/api/java8

英文

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Scanner.html

使用 java.math.BigDecimal

Java的浮点数运算遵守IEEE754浮点数运算(Floating-point arithmetic)规范,使用分数与指数来表示浮点数。

例如,0.5会使用1/2来表示,0.75会使用1/2+1/4来表示,0.875会使用1/2+1/4+1/8来表示,而0.1会使用1/16+1/32+1/256+1/512+1/4096+1/8192+...无限循环下去,无法精确表示,因而造成运算上的误差。

为了更好的精确度,我们可以使用BigDecimal来进行计算 。

		BigDecimal a = new BigDecimal(123);
		// 加法
		a = a.add(new BigDecimal(1));
		// 减法
		a = a.subtract(new BigDecimal(2));
		System.out.println(a);

API地址

英文

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigDecimal.html

对象的相等性

在Java中有两大类型系统,即基本类型与类类型。

因此我们需要区分=与==运算用于基本类型与类类型的不同。

基本类型
  • = 将值复制给变量
  • == 比较两个变量储存的值是否相同

来看一个示例

		int a = 1;
		int b = 1;
		int c = a;
		
		System.out.println(a == b);
		System.out.println(a == c);
类类型
  • = 用在指定引用类型变量引用某个对象
  • == 是用在比较两个引用类型变量是否引用同一对象, =是用在将某个名牌绑到某个对象,而===是用在比较两个名牌是否绑到同一对象。

我们再来看一下

object_class

来看一个示例

		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);

		System.out.println(a.equals(b));
		System.out.println(a.equals(c));

下面是前三行赋值的简单示意图

image-20190503195949555

image-20190503200014547

最后当我们使用!=时候,它和==的正好相反。这里就不多少了。

多说一句。从内存的实际运作来看,=与==并没有用在基本类型与对象类型的不同。

基本类型与对应的类类型

打包基本类型

Java中有两个类型系统,即基本类型与类类型,使用基本类型目的在于效率,然而更多时候,会使用类建立实例,因为对象本身可以携带更多信息。

如果要让基本类型像对象一样操作,可以使用Long、Integer、Double、Float、Boolean、Byte等类来打包基本类型。这些类位于java.lang包中。

这里我们以int对应的Integer为例,来进行讲解。

        int data1 = 10;
        int data2 = 20;
        
        Integer wrapper1 = new Integer(data1);
        Integer wrapper2 = new Integer(data2);
        
        System.out.println(data1 / 3);
        System.out.println(wrapper1.doubleValue() / 3);
        System.out.println(wrapper1.compareTo(wrapper2));

自动装箱、拆箱

除了使用new创建基本类型打包器之外,从J2SE5.0之后提供了自动装箱(Autoboxing)功能。

就是说编译器会自动判断是否能进行自动装箱。不需要我们自己去写代码。

因为有了自动装箱,拆箱,我们把上面的例子,在写一遍

		Integer wrapper1 = 10;
		Integer wrapper2 = 20;

		System.out.println(wrapper1.doubleValue() / 3);
		System.out.println(wrapper1.compareTo(wrapper2));

装箱的原理

自动装箱与拆箱的功能事实上是语法糖,也就是编译程序让你写程序时吃点甜头,编译时期根据所写的语法,决定是否进行装箱或拆箱动作。

比如

Integer i = 100;
// 装箱编译后
Integer i = Integer.valueOf(100);

int j = i;
// 拆箱编译后
int j = i.intValue();

初见null和NullPointerException

在Java程序代码中,null代表一个特殊对象,任何类声明的引用类型变量都可以引用null,表示该名称没有参考至任何对象实体,这相当于有个名牌没有任何人佩戴。

下面看一下的代码

		Integer i = null;
		int j = i;

由于i并没有引用至任何对象,所以就不可能操作intValue()方法,就相当于有个名牌没有人佩戴,你却要求戴名牌的人举手,这是一种错误,在Java中会出现NullPointerException的错误信息。

复习相等性

为了复习对象的相等性,我们使用刚才的学的Integer来练习一下

        Integer i1 = 127;
        Integer i2 = 127;
        if (i1 == i2) {
            System.out.println("i1 == i2");
        }
        else {
            System.out.println("i1 != i2");
        }

数组

当需要保存一组数据的时候,我们就需要用到数组。

数组的定义

        int[] scores = {88, 81, 74, 68, 78, 76, 77, 85, 95, 93};
        for(int i = 0; i < scores.length; i++) {
            System.out.printf("学生分数:%d %n", scores[i]);
        }

数组的操作

另外的例子

        int[][] arr = new int[2][];
          arr[0] = new int[] { 1, 2, 3, 4, 5 };
          arr[1] = new int[] { 1, 2, 3 };
          for (int[] row : arr) {
            for (int value : row) {
              System.out.printf("%2d", value);
            }
            System.out.println();
          }

数组复制

复制, 对吗?

int[] scores = {88, 81, 74, 68, 78, 76, 77, 85, 95, 93};
int[] scores2 = scores;

还是这样?

        int[] scores = {88, 81, 74, 68, 78, 76, 77, 85, 95, 93};
        int[] scores2 = new int[scores.length];
        for(int i = 0; i < scores.length; i++) {
            Sscores2[i] = scores[i];
        }

如果每次复制都这么写,估计的疯。还好Java给我提供了一些API

System.arraycopy()

​ 五个参数分别是来源数组、来源起始索引、目的数组、目的起始索引、复制长度。

Arrays.copyOf() 一般数组复制用这个

​ 两个参数分别是来源数组,和新的数组的长度,返回新的数组。

		int[] scores1 = { 88, 81, 74, 68, 78, 76, 77, 85, 95, 93 };
		int[] scores2 = Arrays.copyOf(scores1, scores1.length);
		for (int score : scores2) {
			System.out.printf("%3d", score);
		}
		System.out.println();
		scores2[0] = 99;
		for (int score : scores1) {
			System.out.printf("%3d", score);
		}

上面我们看的都是基本类型的数组。

我们来看一个引用类型数组的复制

        Clothes[] c1 = {new Clothes("red", 'L'), new Clothes("blue", 'M')};
        Clothes[] c2 = new Clothes[c1.length];
        for(int i = 0; i < c1.length; i++) {
            c2[i] = c1[i];
        }
        c1[0].color = "yellow";
        System.out.println(c2[0].color);

上面的这个复制,我们成为浅层复制(Shallow copy)。

既然有浅层,那就一定会有深层复制(Deep copy)。

我们看一下

        Clothes2[] c1 = {new Clothes2("red", 'L'), new Clothes2("blue", 'M')};
        Clothes2[] c2 = new Clothes2[c1.length];
        for(int i = 0; i < c1.length; i++) {
            Clothes2 c = new Clothes2(c1[i].color, c1[i].size);
            c2[i] = c;
        }
        c1[0].color = "yellow";
        System.out.println(c2[0].color);

字符串对象

在Java中,字符串本质是打包字符数组的对象,是java.lang.String类的实例。

字符串和常用方法
		String name = "test";
		System.out.println(name);
		System.out.println(name.length());
		System.out.println(name.charAt(0));
		System.out.println(name.toUpperCase());
字符串连接
"test" + “test2”
字符串转基本类型

这里以整数Integer为例。

		String num = "123";
		int i = Integer.parseInt(num);
		System.out.println(i);

字符特性

以Java的字符串来说,就有一些必须注意的特性:

  • 字符串常量与字符串池。
  • 不可变动(Immutable)字符串。
字符串常量与字符串池

看下面的代码, 应该返回什么?

		String str1 = new String("test");
		String str2 = new String("test");
		System.out.println(str1 == str2);

下面的这个呢?

		String str1 = "test";
		String str2 = "test";
		System.out.println(str1 == str2);

很意外吧。这代表了str1与str2是引用同一个对象。

在Java中为了效率考虑,以""包括的字符串,只要内容相同(序列、大小写相同),无论在程序代码中出现几次,JVM都只会建立一个String实例,并在字符串池(String pool)中维护。

在上面这个程序片段的第一行,JVM会建立一个String实例放在字符串池中,并给str1参考,而第二行则是让str2直接参考至字符串池中的String实例。

image-20190510212717165

不可变

在Java中,字符串对象一旦建立,就无法更改对象中任何内容,对象上没有任何方法可以更改字符串内容。那么使用+连接字符串是怎么达到的?

		String str1 = "test";
		String str2 = str1+ "test";
    System.out.println( str2);

反编译这段程序,结果会发现:

    11  invokespecial java.lang.StringBuilder(java.lang.String) [26]
    14  ldc <String "test"> [16]
    16  invokevirtual java.lang.StringBuilder.append(java.lang.String) : 			   java.lang.StringBuilder [29]

如果使用+连接字符串,会变成建立java.lang.StringBuilder对象,使用其append()方法来进行+左右两边字符串附加,最后再转换为toString()返回。

最后我们再来看一个无聊的问题

		String text1 = "Ja" + "va";
		String text2 = "Java";
		System.out.println(text1 == text2);

关于转码

返回的字节数组已经是新编码的了。

String a = "测试";
String b = new String(a.getBytes("UTF8"),"GBK");
小总结

简单地说,使用+连接字符串会产生新的String实例,这并不是告诉你,不要使用+连接字符串,毕竟+连接字符串很方便,这只是在告诉你,不要将+用在重复性的连接场合,像是循环中或递归时使用+连接字符串,这会因为频繁产生新对象,造成效能上的负担。