万物皆对象
对象操作
“名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”。(引用自 莎士比亚,《罗密欧与朱丽叶》)。
所有的编程语言都会操作内存中的元素。在C/C++中,对象的操作是通过指针来完成的。
Java利用万物皆对象的思想和单一一致的语法方式来简化问题。我们所操作的标识符只是对对象的“引用”。
下面来创建一个 String 引用,用于保存单词或语句。代码示例:
String s;
这里只是声明了一个**String对象的引用,而非对象。**
只会在内存的栈区(栈内存)创建引用,堆(堆内存)中并无此引用的指向。
也就是说,只会在栈内存中创建引用s,而这个s并没有任何指向,是没有空间的。
直接拿来使用会出现错误:因为此时你并没有给变量 s 赋值--指向任何对象。通常更安全的做法是通过字面值的方式:创建一个引用的同时进行初始化。代码示例:
String s = "asdf"
由于String类型是一种特殊对象,这个时候在内存的栈区(栈内存)创建引用s,在字符串常量池里分配内存空间存放asdf,然后返回地址给引用s,这样s就会指向asdf这个字符串对象
Java 8以后字符串常量池是在堆中的。
也就是说在栈内存中创建一个引用s,并且指向字符串常量池(1.8以后字符串常量值在堆内存中)里的asdf,这样就会占用内存空间。
对象的创建
”引用“用来关联”对象“。在Java中,我们通常使用new操作符来创建一个新的对象。new关键字代表:创建一个对象实例。(实例化)所以,我们也可以这样来表示前面的代码示例:
String s = new String("asdf")
以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了 String 类型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。
数据存储
JVM的内存分配为5个不同地方,分别是
1.寄存器:(Registers)最快的存储区域,位于 CPU 内部 ^2。 但是存储容量十分有限。Java,没有对其的直接控制权,也无法在自己的程序里找到寄存器存在的踪迹。(C/C++允许开发者向编译器建议寄存器的分配)。
2.栈内存:(Stack)存在与常规内存RAM区域中,可通过栈指针获得处理器的直接支持。Java系统必须准确地知道栈内存保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,栈内存上只存储一些基本类型数据和对象引用。栈内存为线程私有的,每个线程都有各自的栈内存,所存储的东西也是不一样的,但是线程和线程之间数据可以共享。
3:堆内存:(Heap)这是一种通用的内存池也存在于常规内存RAM中,所有对象以及常量池都存在于其中。**与栈内存不同的是,编译器不需要知道对象必须在堆内存中停留多长时间。**引起堆内存保存数据更加灵活。创建对象时只需要new命令实例化对象即可,当代码执行时,会自动在堆内存中进行内存分配。但是分配和清理堆内存要比栈内存需要更多的时间。 随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。 堆内存为线程公有的,每个线程都访问同一个堆内存,数据可以共享。
4:常量存储: (Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM中。一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。
5: 非 RAM 存储(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。
基本类型的存储
Java 一共有8中基本数据类型,分别为byte,short,int,long,float,double,char,boolean。
基本数据类型在Java中使用频率很高,它们需要特殊对待。它们不通过new关键字来产生,因为以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方式,Java采用了和C/C++一样的策略。不使用new来创建变量,而是使用一个“自动”变量 。这个变量直接存储“值”,并置于栈内存,因此更加高效。
基本类型默认值
| 基本类型 | 初始值 |
|---|---|
| boolean | false |
| char | \u0000 (null) |
| byte | (byte) 0 |
| short | (short) 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
基本类型与常量池
Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, **但是它们只在大于等于-128并且小于等于127时才使用常量池。 因为Java对于-128到127之间的数,会进行缓存, 超过这个范围就需要创建对象。 **
基本类型存储位置
通常来说基本类型会存在栈中。
比如
void func(){
int a = 3;
}
//这自然是存在栈里的。局部方法嘛。
class Test{
int a = 3;
}
//这就肯定是随对象放到堆里的。
基本类型和==
基本数据类型就是比较值
高精度数值
double d1 = 10.01d;
double d2 = 10.00d
System.out.println(d1-d2);//结果为0.009999999999999787
float f1=10.01f;
float f2=10.00f;
System.out.println(f1-f2);//结果0.010000229
由于double和float运算时会丢失精度,所以Java提供了两种用于高精度的数据类型它们是 BigInteger 和 BigDecimal。但他们没有对应的基本数据类型。 BigInteger 和 BigDecimal也可以做运算,不过要调用它们的方法来实现而非运算符。 此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。 BigDecimal 支持任意精度的定点数字。
数组存储
数组实际上只是内存区块,如果程序访问了数组内存块以外的数组或数组初始化前就使用该内存区域(数组越界异常),结果是不可预测的。
Java的设计主要设计目标之一是安全性。因此在Java中,数组使用前需要被初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价的,但由此换来的安全性和效率的提高是值得的。(但是Java可以优化这些操作)
当我们创建数组对象是,实际上是创建了一个引用数组,并且每个引用的初始值为null。在使用该数组之前,我们必须为每个引用指定一个对象。如果我们尝试使用为null的引用,则会在运行时报错。因此,在Java中就防止了数组操作的常规错误。
我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始化。
高精度数值
double d1 = 10.01d;
double d2 = 10.00d
System.out.println(d1-d2);//结果为0.009999999999999787
float f1=10.01f;
float f2=10.00f;
System.out.println(f1-f2);//结果0.010000229
由于double和float运算时会丢失精度,所以Java提供了两种用于高精度的数据类型它们是 BigInteger 和 BigDecimal。但他们没有对应的基本数据类型。 BigInteger 和 BigDecimal也可以做运算,不过要调用它们的方法来实现而非运算符。 此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。 BigDecimal 支持任意精度的定点数字。
代码注释
Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 /* 开头,可以跨越多行,到 */结束。注意,许多程序员在多行注释的每一行开头添加 *,所以你经常会看到:
/* 这是
* 跨越多行的
* 注释
*/
但请记住,/* 和 */ 之间的内容都是被忽略的。所以你将其改为下面这样也是没有区别的。
/* 这是跨越多
行的注释 */
第二种注释形式来自 C++ 。它是单行注释,以 // 开头并一直持续到行结束。这种注释方便且常用,因为直观简单。所以你经常看到:
// 这是单行注释
对象清理
作用域
作用域决定了该范围内定义的变量名的可见性和生命周期。在Java中,作用域是由大括号{}的位置决定的。例如:
//作用域起点
{
int x = 12;
// 仅 x 变量可用
{
int q = 96;
// x 和 q 变量皆可用
}
// 仅 x 变量可用
// 变量 q 不在作用域内
}
//作用域终点
Java的变量只有在其作用域内才可用。 由于 Java 是一种自由格式的语言,额外的空格、制表符和回车并不会影响程序的执行结果。 缩进使得Java代码更易于阅读。
在Java中不能重复定义变量,列入:
{
int x = 12;
{
int x = 96; // Illegal
}
}
Java编译器会提示变量x已经被定义过了。因为Java的设计者认为这样会导致程序混乱,不宜阅读。
对象作用域
Java对象与基本类型具有不同的生命周期。当我们使用new关键字来创建Java对象时,它的生命周期将会超出作用域。
{
String s= new String("str")
}
上例中,引用s在作用域终点前就结束了。但是,引用s指向的字符串对象依然还在占用内存。虽然这个对象依然在占用内存,但是我们无法在这个作用域之后访问这个对象,因为对它唯一的引用s已超出作用域的范围。
new出来的对象会一直在内存中存活下去,直到Java垃圾收集器检查出该对象不再可用,才会释放那些被占用的内存,供其他新对象使用。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
类的创建
类型
大多数面向对象语言都使用class关键字来描述一种类型,class关键字的后面紧跟类的名称。
class ATypenae{
//类的内部
}
在上例中,引入了一个新的类型,尽管类里面只有一行注释。但是,仍然可以通过new关键字来创建这个类型的对象,如下:
ATypename a = new ATypename();
现在还不能用这个对象来做什么事情,我们需要在类里面添加一些东西。
字段
当我们创建好一个类后,我们可以往类里存放两种元素:方法(method)和字段(field)。类的字段可以是基本类型,也可以是引用类型。如果类的字段是某个对象的引用,那么必须初始化该引用将其关联到一个实际的对象。
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.student.study();
}
}
class Person {
Student student;//这样添加引用字段如果想访问student对象里的方法或者数据必须要初始student。
Student student =new Student();//要初始化。
}
class Student {
public Student() {
}
void study() {
System.out.println("好好学习");
}
}
每个对象都有用来存储其字段的空间。通常,字段不在对象间共享。下面是一个具有某些字段的类的代码:
class DateOnly{
int i;
double d;
boolean b;
}
这个类除了存储数据之外什么也不能做。但是,我们仍然可以创建它的一个对象。
DateOnly date = new DateOnly();
我们必须通过这个对象的引用来指定字段值。格式:引用名.方法名或字段名。
date.i = 47;
date.d = 47.7;
date.b = true;
如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例:
myPlane.leftTank.capacity = 100;
你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱)。
到现在为止,我们还不能用这个对象来做什么事(即不能向它发送任何有意义的消息),除非我们在这个类里定义一些方法。
基本类型字段
如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型会将被赋予一个初始值。这些默认值仅在初始化类的时候才会被赋予。这种方式确保了基本类型的字段始终能被初始化,从而减少了bug的来源。但是,这些初始值对程序来说不一定是合适或者正确的。所以我们最好显式地初始化变量。
这种默认值的赋予并不适用于局部变量——那些不属于类的字段变量。因此,若在方法中定义基本类型数据,则不会自动初始化。
方法使用
在Java中,方法决定对象能接收那些消息。方法的基本组成 包括名称、参数、返回类型、方法体。格式如:
[返回类型][方法名](/*参数列表*/){
//方法体
}
返回类型
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为方法签名。签名作为方法的唯一标识。
Java中方法只能作为类的一部分创建。它只能被对象所调用,并且该对象必须有权来执行调用。若对象调用错误的方法,则程序将在编译时报错。
我们可以像下面这样调用一个对象的方法:
[对象引用].[方法名称](参数1,参数2)
若方法不带参数,但有返回值,我们可以如下表示:
int x = a.f();
上列中对象引用a调用该对象的f不带参数,返回值为int类型的方法。f的返回值类型必须和变量x的类型兼容调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发消息。
参数列表
方法参数列表制定了传递给方法的信息,这些信息就像Java中的其他所有信息,以对象的形式传递。**参数列表必须指定每个对象的类型和名称。**同样,我们并没有直接处理对象,而是在传递对象引用。通常除了前面提到的“特殊”数据类型 boolean、 char、 byte、 short、 int、 long、 float 和 double。通常来说,传递对象就意味者传递对象的引用。 但是引用的类型必须正确。如果方法需要String类型参数那么,则必须传入String类型。否则编译器报错。
int storage(String s){
return s.length() * 2;
}
此方法计算并返回传入字符串所占的字节数。参数s的类型为String。将s传递给storage()后,我们可以把它看作和任何其他对象一样,可以向它发送消息,我们调用leagth()方法,他是String的一个方法,返回字符串中的字符数。字符串中每个字符的大小为16位或2个字节。 你还看到了 return 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就紧跟 return 语句之后。这里,返回值是通过计算
s.length() * 2
产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 void 来表明这是一个无需返回值的方法。 代码示例:
boolean flag() {
return true;
}
double naturalLogBase() {
return 2.718;
}
void nothing() {
return;
}
void nothing2() {
}
当返回类型为 void 时, return 关键字仅用于退出方法,因此在方法结束处的 return 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 void,则编译器会强制我们返回相应类型的值。
程序编写
命名可见性
Java采取了一种很有创造性的方法:来为一个类库生成一个明确的名称。
Java创建者希望我们反向并小写我们的网络域名,因为域名通常是唯一的。
使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 com.mindviewinc.utility.foibles 这样的目录结构中,我们创建了 com 和 mindviewinc 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。
这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。
使用其他组件
无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义。
import关键字来告诉Java编译器具体要使用的类。import指示编译器导入一个包,也就是一个类库。大多数时候,我们都在使用Java标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如:
import java.util.ArrayList;
上例可以告诉编译器使用位于标准库 util 下的 ArrayList 类。但是,util 中包含许多类,我们可以使用通配符 * 来导入其中部分类,而无需显式得逐一声明这些类。代码示例:
import java.util.*;
static关键字
在Java中我们在类的字段或方法前添加static关键字来表示这是一个静态字段或者静态方法。
当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例。即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 。静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用)。) 。
为什么要存在静态方法或者事物呢?
- 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
- 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
这时候我们就需要通过static关键字来创建对象。
我们在StaticTest类里面添加了一个值为44,引用名为i的静态int类型数据。
class StaticTest{
static int i=44;
}
现在,即使你创建了两个 StaticTest 对象,但是静态变量 i 仍只占一份存储空间。两个对象都会共享相同的变量 i。 代码示例:
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
st1.i 和 st2.i 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来定位它,例如 st2.i。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行:
StaticTest.i++;
++ 运算符将会使变量结果 + 1。此时 st1.i 和 st2.i 的值都变成了 48。
使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 Classname.method() 来直接调用静态字段或方法 (在某些情况下,它还为编译器提供了更好的优化可能。) 。 代码示例:
class Incrementable {
static void increment() {
StaticTest.i++;
}
}
上例中,Incrementable 的 increment() 方法通过 ++ 运算符将静态数据 i 加 1。我们依然可以先实例化对象再调用该方法。 代码示例:
Incrementable sf = new Incrementable();
sf.increment();
当然了,首选的方法是直接通过类来调用它。代码示例:
Incrementable.increment();
相比非静态的对象,static 属性改变了数据创建的方式。同样,当 static 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,static 关键字的这些特性对于应用程序入口点的 main() 方法尤为重要。
main方法入口
每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 main() 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
public static void main(String[] args) {
}
关键字 public 表示方法可以被外界访问到。 main() 方法的参数是一个 字符串(String) 数组。 参数 args 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有, 这是因为**它们被用于接收从命令行输入的参数。 **
编码风格
Java 编程语言编码规范(Code Conventions for the Java Programming Language)要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
class AllTheColorsOfTheRainbow {
// ...
}
有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:
class AllTheColorsOfTheRainbow {
int anIntegerRepresentingColors;
void changeTheHueOfTheColor(int newHue) {
// ...
}
// ...
}