文章目录
编程语言中的数据类型
Java中的类型与变量
类型是一组值,以及可以对这些值执行的操作。
变量用特定数据类型定义,可存储满足类型约束的值。
Java原始类型基本数据类型:int
, long
, boolean
, double
, char
等
Java对象数据类型:String
, BigInteger
等
根据Java约定,基本体类型为小写,而对象类型以大写字母开头。
-
原始数据类型:只有值,没有ID(无法区分);不可变的 ;在栈中分配内存;代价低
-
对象数据类型:既有ID,也有值;可变或不可变;在堆中分配内存;代价昂贵
对象类型形成层次结构
所有非原始类型的根类型都为Object
如果省略了extends
语句,则默认继承自Object
一个类是其所有父级类的一个具体类型
- 类可以从它的父级类中继承可见的字段和方法
- 类可以覆盖继承的方法来改变它们的行为
包装原始类型
Boolean
, Integer
, Short
, Long
, Character
, Float
, Double
将基本类型包装为对象类型,通常是在定义集合类型的时候使用它们,一般情况下,尽量避免使用。
一般可以自动转换。
操作符&操作
操作符
操作符:执行简单计算的符号,如 =
,+
,-
,*
,/
操作符优先级:遵循标准的数学规则。
字符串连接(+):
String text = "hello" + " world";
text = text + " number " + 5;
// text = "hello world number 5"
操作
操作是接受输入和产生输出的函数(有时也会改变值本身)。
-
作为中缀、前缀或后缀运算符。例如,
a + b
调用操作=
: int × int → int 。 -
作为一个对象的一种方法。例如,
bigint1.add(bigint2)
调用操作add
:BigInteger × BigInteger → BigInteger 。 -
作为一个函数。例如,
Math.sin(theta)
调用操作sin
:double → double 。在这里,Math不是一个object。它是一个包含sin函数的类。
重载
有些操作被重载,因为相同的操作名称用于不同类型。
对于Java中的数字原始类型,算术运算符+、-、*、/被大量重载。
方法也可以被重载。大多数编程语言都有一定程度的重载。
静态与动态数据类型检查
类型转换
int a = 2; // a = 2
double a = 2; // a = 2.0 (隐式类型转换)
int a = (int) 18.7; // a = 18
double a = (double)2/3; // a = 0.6666…
int a = 18.7; // ERROR
String a = 1; // ERROR
double a = 2/3; // a = 0.0
静态类型 vs 动态类型
Java是一种静态类型语言:
-
所有变量的类型在编译时(在程序运行之前)都是已知的,因此编译器也可以推断出所有表达式的类型。
-
如果a和b被声明为int,那么编译器得出结论,a+b也是int。
-
Eclipse 或 IDEA 在编写代码时执行静态类型检查
(在编译阶段进行类型检查)
在像Python这样的动态类型语言中,这种检查会延迟到程序运行时。(在运行阶段进行类型检查)
一种语言可以提供的三种自动检查:
- 静态类型检查:在程序运行之前就会自动发现错误。
- 动态类型检查:在执行代码时会自动发现错误。
- 不检查:该语言根本不能自动找到错误。
不用说,静态地捕捉一个bug比动态地捕捉它要好,而动态地捕捉它也比根本不捕捉它要好。静态类型检查 >> 动态 >> 无检查
静态类型检查
静态类型检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性。
- 语法错误:比如额外的标点符号或假词。即使是像Python这样的动态类型语言,也会进行这种静态检查。
- 错误地命名了类名/函数名错误,如Math.sine (2)。(正确的名字是sin)
- 参数数量错误,比如Math.sin(30,20)。
- 参数类型错误,如Math.sin(“30”)。
- 返回值类型错误,如从声明返回int的函数返回了“30”。
动态类型检查
- 非法参数值。例如,整数表达式
x / y
只有在y
实际上是0时才是错误的;否则它就能工作。所以在这个表达式中,除以零不是一个静态错误,而是一个动态错误。 - 非法的返回值,即当特定的返回值不能在该类型中表示时。
- 超出范围的索引越界,例如,在字符串上使用一个负的或太大的索引。
- 调用空对象引用上的方法。(空指针)
静态检查 vs 动态检查
- 静态检查倾向于是关于类型的,这些错误与变量所具有的特定值无关。
- 静态类型保证了一个变量将从该集合中有一些值,但是直到运行时我们才知道它到底有哪个值。
- 因此,如果错误只由某些值引起,如除零或索引超出范围,那么编译器就不会对它产生静态错误。
- 相比之下,动态检查往往是关于由特定值引起的错误。
- 总结:静态检查——关于“类型”的检查,不考虑值;动态检查——关于“值”的检查。
可变性和不可变性
可变性,不变性
改变一个变量、改变一个变量的值,二者有何区别?
- 改变一个变量:将该变量指向另一个值的存储空间
- 改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
变化是“罪恶”,但程序不能没有变化。尽可能避免变化,以避免副作用。
不变性是一种重要设计原则。
不变数据类型:一旦被创建,其值不能改变
如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变。
不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用
final
如果想要让一个引用类型不变,使用final
进行修饰:
final int n = 5;
final Person a = new Person(“Ross”);
如果编译器无法确定final
变量不会改变,就提示错误,这也是静态类型检查的一部分。
所以,尽量使用final
变量作为方法的输入参数、作为局部变量。final
表明了程序员的一种“设计决策”。
final
类无法派生子类final
变量无法改变值/引用final
方法无法被子类重写
String & StringBuilder
String
:
String
是不可变类型- 要在字符串的末尾添加一些内容,您必须创建一个新的字符串对象
StringBuilder
:
StringBuilder
是可变类型- 它可以删除字符串的部分、插入或替换字符等。▪
- 这个类有更改对象的值的方法,而不仅仅是返回新的值:
区别,优缺点
当有多个引用的时候,差异就出现了。
可变类型优点
-
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)。
-
可变类型最少化拷贝以提高效率。
-
使用可变数据类型,可获得更好的性能。
-
也适合于在多个模块之间共享数据。
不可变类型优点
- 不可变类型更“安全”,在其他质量指标上表现更好。
实际使用时需要折中考虑,取决于看重哪个质量指标。
使用可变数据类型时防范风险
-
防御式拷贝(安全,但容易造成内存浪费)
在此种情况下使用不可变类型可以防范风险,节省复制代价
-
安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用,则使用可变类型则就不安全
快照图
快照图表示一个程序在运行时的内部状态——它的堆栈(正在进行中的方法及其本地变量)和它的堆(当前存在的对象)。
快照图用于描述程序运行时的内部状态。
为什么使用快照图?
- 便于程序员之间的交流
- 便于刻画各类变量随时间变化
- 便于解释设计思路
快照图中的原始值和对象值
基本类型的值
基本类型值用常数表示。传入的箭头是对变量或对象字段中的值的引用。
对象类型的值
对象类型的值是用其类型标记的圆。
当我们想要显示更多细节时,我们在其中写字段名,箭头指向它们的值。对于更详细信息,字段可以包括它们声明的类型。
重新分配值
不可变对象
不可变对象:用双线椭圆
String s = "a";
s = s + "b";
可变对象
可变对象:单线椭圆
StringBuilder sb = new StringBuilder("a");
sb.append("b");
不可重新分配的/不可变的引用
在快照图中,不可重新分配的引用双箭头表示。
注意:
-
引用是不可变的,但指向的值却可以是可变的。
final StringBuilder sb;
-
可变的引用,也可指向不可变的值。
String s;
复杂的数据类型:数组和集合
Array
数组是另一种类型T的固定长度的序列。
int[]数组类型包括所有可能的数组值,但一个特定的数组值一旦创建,就永远不能更改其长度。
对数组类型的操作包括:
- indexing:
a[2]
- assignment:
a[2] = 0
- length:
a.length
List
列表是另一种类型T的可变长度序列。
对列表的一些操作:
- indexing:
list.get(2)
- assignment:
list.set(2, 0)
- length:
list.size()
Note:
List
是一个接口。2.List
中的成员必须是一个对象。
列表迭代
int max = 0;
for (int x : list) {
max = Math.max(x, max);
}
Set
集合是包含零个或多个唯一对象的无序集合。
对象不能在集合中出现多次。
对集合的一些操作:
s1.contains(e) //test if the set contains an element
s1.containsAll(s2) //test whether s1 ⊇ s2
s1.removeAll(s2) //remove s2 from s1
Set
是一个接口。
Map
映射类似于一个字典(键-值)。
对映射的操作:
map.put(key, val) //add the mapping key → val
map.get(key) //get the value for a key
map.containsKey(key) //test whether the map has a key
map.remove(key) //delete a mapping
Map
是一个抽象接口。
迭代器
迭代器是一个对象,它逐步通过元素集合并逐个返回元素。在Java中,当使用为(… : …)循环遍历列表或数组时,将使用迭代器。
一个迭代器有两个方法:
next()
返回集合中的下一个元素,这是一个mutator方法hasNext()
测试迭代器是否已到达集合的结束。
在使用迭代器过程中,需要注意使用某些方法修改了列表对象时对迭代器状态的破坏。
实用的不可变类型
基本类型及其封装对象类型都是不可变的。
不要使用可变的Date
,请根据您需要的计时粒度使用java中适当的不可变类型。
List
, Set
, Map
的具体实现类都是可变的,Collection
类有一些方法来获取这些可变集合的不可修改的视图:
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableMap
实际上是基于list
,set
,map
的包装器。
这种包装器得到的结果是不可变的:只能读不能写。
构造的包装器抛弃了原来的mutator方法,在调用时会抛出异常。
但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查。
不可修改的包装器通过拦截所有将修改集合的操作,并抛出一个不受支持的操作异常,从而剥夺了修改集合的能力。
不可修改的包装器有两个主要用途,如下:
- 在集合构建后使其不可变。
- 允许某些客户端只读访问您的数据结构。
要从某些已知值创建不可变集合,请使用List.of
、Set.of
和Map.of
List<String> a = List.of("lion", "tiger", "bear");
或者,使用List.copyOf
(Java 10)来创建不可修改的可变集合。