软件构造系列学习笔记(3.1)—————数据类型和类型检查

本文探讨了编程中的数据类型,包括Java中的基本和对象类型,强调了类型检查的重要性,对比了静态和动态类型语言的区别。文章还讨论了不变性和可变性在软件设计中的角色,以及在使用可变类型时可能出现的问题。最后,提到了数组、集合和不可变类型的使用,以及在Java中如何处理null引用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据类型和类型检查

前两章回答了:什么是“高质量的软件”、 如何从不同维度刻画软件、软件构造的基本过程和步骤。
本章主要内容是软件构造的理论基础——ADT(抽象数据类型) 和软件构造的技术基础——OOP(面向对象编程)。

目录

  • Data type in programming languages
  • Static vs. dynamic data types
  • Type checking
  • Mutability & Immutability
  • Snapshot diagram
  • Complex data types: Arrays and Collections
  • Useful immutable types
  • Null references

Data type in programming languages

Types and Variables

数据类型(type)是一组值,以及可以对这些值执行的操作。例如:

  • boolean: Truth value (true or false)
  • int: Integer (0, 1, -47)
  • double: Real number (3.14, 1.0, -2.1)
  • String: Text (“hello”, “example”)
    变量(Variables):用特定数据类型定义,可存储满足类型约束的值。形式:TYPE NAME

Types in Java

primitive types 基本数据类型,例如:

  • int (for integers like 5 and -200, but limited to the range ± 2^31, or roughly ± 2 billion)
  • long (for larger integers up to ± 2^63)
  • boolean (for true or false)
  • double (for floating-point numbers, which represent a subset of the real numbers)
  • char (for single characters like ‘A’ and ‘$’ )

object types 对象数据类型,例如:

  • String represents a sequence of characters
  • BigInteger represents an integer of arbitrary size

按照Java约定,基本类型是小写字母,而对象类型以大写字母开头。

这里写图片描述

对象类型形成层次结构

所有对象的根结点都是 Object
这里写图片描述

Boxed primitives

将基本类型包装为对象类型,例如Boolean, Integer, Short, Long, Character, Float, Double。通常是在定义集合类型的时候使用它们。一般情况下,尽量避免使用。一般可以自动转换。

Operations

操作符:执行简单计算的符号。例如+,-,*,/,=。
操作:是获取输入并生成输出的函数(有时会自行更改这些值)。
重载(Overload):同样的操作名可用于不同的数据类型 。


Static vs. dynamic data types

Java是静态类型语言 ,在编译阶段进行类型检查 ,而动态类型语言,例如Python,在运行阶段进行类型检查。

Type checking

Conversion by casting 类型转换

这里写图片描述

Static Checking and Dynamic Checking

一种语言可以提供的三种自动检查:

  • 静态类型检查:该程序甚至在运行之前自动发现该错误
  • 动态类型检查:该代码执行时会自动发现该错误
  • 无检查:该语言不会帮助您找到错误。 必须亲自检查,否则最终会得到错误的答案。

毫无疑问,静态类型检查 >> 动态 >> 无检查。

静态类型检查

静态类型检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性。具体分为以下几个错误:

  • Syntax errors(语法错误)
  • Wrong names (类名/函数名错误)
  • Wrong number of arguments (参数数目错误)
  • Wrong argument types (参数类型错误)
  • Wrong return types (返回值类型错误)
动态类型检查

在运行时检查是否有错,具体分为以下几个错误:

  • Illegal argument values (非法的参数值)
  • Unrepresentablereturn values (非法的返回值)
  • Out-of-range indexes (越界)
  • Calling a method on a null object reference(空指针)

Static vs. Dynamic Checking

静态检查:关于“类型”的检查,不考虑值 。
动态检查:关于“值”的检查。


Mutability and Immutability

改变一个变量、改变一个变量的值,二者有何区别?

  • 改变一个变量:将该变量指向另一个值的存储空间 。
  • 改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。

Immutability 不变性

不变性是一个重要设计原则 。

不变数据类型(Immutable types ):一旦被创建,其值不能改变 。如果是引用类型,也可以是不变的,即一旦确定其指向的对象,不能再被改变 。要使引用不可变,请使用关键字final声明它。如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。

所以,尽量使用 final变量作为方法的输入参数、作为局部变量。 final表明了程序员的一种“设计决策”。

Note:

  • final类无法派生子类
  • final变量无法改变值/引用
  • final方法无法被子类重写

不变对象:一旦被创建,始终指向同一个值/引用 。
可变对象:拥有方法可以修改自己的值/引用。

String as an immutable type
String s = "a";
s = s.concat("b");//s+="b" and s=s+"b" also mean the same thing

snapshot:
这里写图片描述

可见,s指向了一个新的引用。

StringBuilder as a mutable type
StringBuilder sb = new StringBuilder("a");
sb.append("b");

snapshot:
这里写图片描述

可见sb指向的存储空间并没有改变,只是里面存储的值改变了。

上面两个例子尽管最终结果相同,可见当只有一个引用指向该值, mutable和immutable的数据类型并没有区别, 但有多个引用的时候,差异就出现了 。

String t = s;
t = t + "c";

StringBuilder tb = sb;
tb.append("c");

snapshot:
这里写图片描述

Advantage of mutable types

使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收) ;而可变类型最少化拷贝以提高效率 。使用可变数据类型,可获得更好的性能 ;也适合于在多个模块之间共享数据

既然如此,为何还要用不可变类型?

答案是不可变类型更“安 全”,在其他质量指标上表现更好 。

Risky example #1: passing mutable values

首先看一个计算list中所有整数和的方法

/** @return the sum of the numbers in the list */
public static int sum(List<Integer> list) {
    int sum = 0;
    for (int x : list)
        sum += x;
    return sum;
}

假设我们还需要一个计算所有整数绝对值总和的方法。 遵循良好的DRY练习(不要重复自己),实现者编写一个使用sum()的方法:

/** @return the sum of the absolute values of the numbers in the list */
public static int sumAbsolute(List<Integer> list) {
    // let's reuse sum(), because DRY, so first we take absolute values
    for (int i = 0; i < list.size(); ++i)
        list.set(i, Math.abs(list.get(i)));
    return sum(list);
}

接着调用main函数

// meanwhile, somewhere else in the code...
public static void main(String[] args) {
    // ...
    List<Integer> myData = Arrays.asList(-5, -3, -2);
    System.out.println(sumAbsolute(myData));
    System.out.println(sum(myData));
}

结果是什么?如果你认为是10和-10就错了,打印后的结果两个都为10。原因是List中的Integer类型的数据是mutable的,而sumAbsolute函数改变了list中数的正负值,因此之后sum计算的数是改变后的数,而不是原始数据。 这种错误非常难于跟踪和发现 , 对其他程序员来说,也难以理解。

Risky example #2: returning mutable values

我们刚刚看到一个将可变对象传递给函数的示例导致了问题。如果返回一个可变的对象会有怎样的风险呢?
我们来看看Date,它是内置的Java类之一。 Date恰好是一个可变类型。

/** @return the first day of spring this year */
public static Date startOfSpring() {
    return askGroundhog();
}

这里我们使用众所周知的Groundhog算法来计算春季何时开始(Harold Ramis,Bill Murray等人,土拨鼠日,1993)。

客户开始使用这种方法,例如计划他们的大派对:

// somewhere else in the code...
public static void partyPlanning() {
    Date partyDate = startOfSpring();
    // ...
}

之后,代码被重写为最多询问一次groundhog,然后将groundhog的答案缓存以备将来调用:

/** @return the first day of spring this year */
public static Date startOfSpring() {
    if (groundhogAnswer == null) groundhogAnswer = askGroundhog();
    return groundhogAnswer;
}
private static Date groundhogAnswer = null;

其次,startOfSpring()的客户之一决定,春天的实际第一天对于聚会来说太冷,所以聚会将在一个月后。

// somewhere else in the code...
public static void partyPlanning() {
    // let's have a party one month after spring starts!
    Date partyDate = startOfSpring();
    partyDate.setMonth(partyDate.getMonth() + 1);
    // ... uh-oh. what just happened?
}

来看一下snapshot,上面的代码会导致什么错误。
这里写图片描述

可见,groundHogAnswer和partyDate都指向同一个Date对象,当partyDate改变month时,会导致groundHogAnswer的month也发生改变。
如何防止此类错误呢?可以采用防御式拷贝(defensive copy)

return new Date(groundhogAnswer.getTime()); 

通过防御式拷贝,给客户端返回一个全新的Date对象 。但是。大部分时候该拷贝不会被客户端修改, 可能造成大量的内存浪费 。因此,如果使用不可变类型, 则节省了频繁复制的代价,由此可以看到不可变类型的好处。

什么时候可以安全地使用可变类型对象呢?

  • 局部变量,不会涉及共享
  • 只有一个引用

若有多个引用(别名),使用可变类型就非常不安全 。


Snapshot diagram

Snapshot 是一个code-level, run-time, and moment view。用于描述程序运行时的内部状态 ,好处是用于描述程序运行时的内部状态 ,便于刻画各类变量随时间变化 ,便于解释设计思路

Primitive and Object values in Snapshot Diagram

  • Primitive values 基本类型的值
    基本类型的值由bare constants表示。
    这里写图片描述

  • Object values 对象类型的值
    对象类型的值是由其类型标记的圆
    这里写图片描述

不可变对象,用双线椭圆。

String s = "a";
s = s + "b"; 

这里写图片描述

不可变引用(Immutable references)
用双线箭头
这里写图片描述

引用是不可变的,但指向的值却可以是可变的。可变的引用,也可指向不可变的值。


Complex data types: Arrays and Collections

Array

数组是另一个类型T的固定长度序列。

int[] a = new int[100]; 

int []数组类型包含所有可能的数组值,但是一旦创建了特定的数组值,永远不会改变其长度。
数组类型的操作包括:

  • indexing: a[2]
  • assignment: a[2]=0
  • length: a.length

List

List是另一个类型T的可变长度序列。

List<Integer> list = new ArrayList<Integer>(); 

List的操作包括:

  • indexing: list.get(2)
  • assignment: list.set(2, 0)
  • length: list.size()

需要注意的是List是一个接口,在List中的成员必须是一个对象object。

Iterating

  • 遍历Array
int max = 0; 
for (int i=0; i<array.length; i++) { 
    max = Math.max(array[i], max); 
}
  • 遍历List
int max = 0; 
for (int x : list) { 
    max = Math.max(x, max); 
}

Set

Set是零个或多个唯一对象的无序集合。Set是一个抽象接口。

这里写图片描述

Map

A Map is similar to a dictionary (key-value) ,Map是一个抽象接口。
这里写图片描述

创建List, Set, and Map变量

List<String> firstNames = new ArrayList<String>(); 
List<String> lastNames = new LinkedList<String>();

List<String> firstNames = new ArrayList<>(); 
List<String> lastNames = new LinkedList<>();

Set<Integer> numbers = new HashSet<>();

Map<String,Turtle> turtles = new HashMap<>();

遍历:

List<String> cities = new ArrayList<>(); 
Set<Integer> numbers = new HashSet<>(); 
Map<String,Turtle> turtles = new HashMap<>();
for (String city : cities) { 
    System.out.println(city); 
}
for (int num : numbers) { 
    System.out.println(num); 
}
for (int ii = 0; ii < cities.size(); ii++) {
    System.out.println(cities.get(ii));                                     
}
for (String key : turtles.keySet()) { 
    System.out.println(key + ": " + turtles.get(key)); 
}

迭代器(Iterator)

迭代器是一个mutable的对象,它可以逐步收集元素并逐个返回元素。

iterator有两种方法:

  • next():返回集合中的下一个元素—这是一个mutator方法!
  • hasNext():测试迭代器是否已达到集合的末尾。

Useful immutable types

基本类型及其封装对象类型都是不可变的。

不要使用可变的Date,根据您需要的计时粒度,使用java.timejava.time.ZonedDateTime中适当的不可变类型。

Java的集合类型(List,Set,Map)的通常实现都是可变的:ArrayList,HashMap等。

Collections实用程序类具有获取这些可变集合的不可修改视图的方法:

  • Collections.unmodifiableList
  • Collections.unmodifiableSet
  • Collections.unmodifiableMap

这种包装器得到的结果是不可变的:只能看,但是这种“不可变”是在运行阶段获得的,编译阶段 无法据此进行静态检查。

Null references

在Java中,对对象和数组的引用也可以采用特殊值Null,这意味着引用不指向对象。 空值是Java类型系统中的一个不幸的漏洞。

基本数据类型元素不能为null,编译器会拒绝这种带有静态错误的尝试:

int size  = null;  //illegal 

可以将null分配给任何非原始变量,并且编译器在编译时高兴地接受这些代码。 但是你会在运行时遇到错误,因为你不能调用任何方法或者使用带有这些引用之一的任何字段(抛出NullPointerExceptions)。

String name = null;       name.length(); 
int[] points = null;      points.length;

null与空字符串“”或空数组不同

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值