目录
引用的作用
数据的储存
常见的数据储存方式
特殊储存的基本类型
数组
销毁对象
基本类型的作用域
对象的作用域
创建新类型 - class关键字
方法、参数和返回值
参数列表
编写程序
名称可见性
使用组件
static关键字
Java程序
编程风格(驼峰式命名法)
本笔记参考自: 《On Java 中文版》
不同于C++的向后兼容,Java的设计预期就是编写面向对象的程序。因此,在进行程序的编写之前,我们需要先熟悉面向对象的知识。
引用的作用
在编程中,我们往往需要通过内存来处理各种元素,比如C/C++使用指针进行间接的内存操作。而Java将一切视为对象,通过对象来进行内存的操作。虽然如此,但在实际操作中,我们会使用到的其实是该对象的引用。
例如,如果我们想要储存一句话,我们就会需要使用到一个String类型的引用:
String s;
上述的语句并没有创建一个对象,而是创建了一个引用。此时还不能向 s 发送信息,因为这个引用还没有连接任何对象。比较安全的做法是在创建引用时对其进行初始化:
String s = "初始化的内容";
这就是引用的作用:关联对象。另外,上述这种初始化的方式(使用带引号的文本)是String类独有的,创建、初始化对象更常用的方式是使用new关键字:
String s = new String("初始化的内容");
除去Java的预置类型,我们也可以手动进行新类型的创建。
数据的储存
常见的数据储存方式
数据储存方式 | 特点 |
---|---|
寄存器 | 速度最快的数据储存方式,将数据直接保存在中央处理器中。(在Java中,寄存器无法由程序员进行分配) |
栈 | 效率仅次于寄存器。不过Java要求明确所以栈上对象的生命周期,这在一定程度上限制了其的灵活性。多用于存放对象引用。 |
堆 | 用于存放所有的Java对象。编译器不关心堆上的对象的生命周期,所以堆的使用更为灵活。 |
常量存储 | 常量通常直接保存在程序代码中,它们的值不会改变。(有时常量会与代码分开保存,此时常量可以保存在只读存储器中) |
非RAM存储 | 这种数据不保存在应用程序中,它们的生命周期不依赖于应用程序。通常,这种数据被保存在其他媒介中。 |
特殊储存的基本类型
new关键字是在堆上创建对象,在面对一些简单的变量时,这种做法就显得不够高效。Java的做法是创建“自动变量”,这种变量会直接储存在栈上面,运行效率较高。
Java定义了每一种基本类型占用的空间大小,这种规定的一致性也给予了Java更好的可移植性。
当然,如果遇到要在堆上开辟基本类型对象的情况,Java也提供了对应的“包装类”。通过包装类,可以把基本类型转变为位于堆上的非原始对象:
// 将基本类型转变为对应的包装类对象
char c = 'x';
Cahracter ch = new Character(c);
// 直接使用包装类进行初始化
Character ch = new Character('x');
// Java的“自动装箱”机制,能够将基本类型自动转换为包装类对象
Character ch = 'x';
反过来,将对象转变为基本类型也是可以的:
char c = ch;
Java提供了两个类来支持高精度计算,分别是BigInteger和BigDecimal。这两个类都没有对应的基本类型。
数组
和许多的编程语言一样,Java也支持数组。但不同于C和C++中数组就是内存块的概念,Java对数组的要求更加严格,也更加安全。在Java中,数组有以下特征:
- 一定被初始化;
- 无法访问数组边界之外的元素。
这使得Java的数组拥有更大的代价,但从安全性等角度来看,这是值得的。
和之前提到的概念相吻合,Java的数组实际上还是存放着引用。Java会将这些引用初始化成特殊值:null,以防止某些数组问题的出现。(如果一个数组用来被放置基本类型,那么这些元素的初识值将会是0)
销毁对象
在C和C++中,对象需要手动销毁,由程序员来回收内存,这会导致一些难以发觉的问题。而Java对内存的自动释放也在这一层面上帮助程序员分担了压力。
基本类型的作用域
||| 作用域的概念:作用域会决定其范围内定义的变量名的可见性和生命周期。
Java通过大括号({ })来定义作用域的范围,如下所示:
{
int a = 12;
// 此时只有变量a是存在并且可以使用的
{
int b = 100;
// 此时a、b都存在
}
// 此时超出了b的作用域,只有a存在
}
但值得注意的是,Java不会运行在外围数据域中“隐藏”变量,这被认为是会引发歧义的:
{
int x = 10;
{
int x = 20; // 出现语法错误
}
}
对象的作用域
与基本类型不同,在Java中,若使用new关键字进行对象的创建,该对象在作用域结束后仍会存在(但是作为连接的引用却会消失):
{
String s = new String("内容");
}// 作用域结束,s被销毁
当作用域结束时,引用s会被销毁,但其连接的对象仍会占用一块内存。此时已经无法再次使用这一占用内存的对象,因为唯一的引用s已经离开了它的作用域。这一情况如果发生在C和C++中,无疑是令人头疼的。但Java会通过自带的垃圾收集器监视所有对象,并及时销毁无用对象。这就解决了“内存泄漏”这一严重的编程问题。
创建新类型 - class关键字
大多数的面向对象编程语言会使用“class”关键字来描述新类型,其用法如下:
class AtypeName{
// 类的具体实现
}
在创建了一个新类型后,我们就可以通过new关键字创建一个该类的对象:
AtypeName a = new AtypeName();
当然,上述的类还缺少很多东西,这些都需要程序员进行补齐。一个类可以有两种元素,分别为:字段(数据成员)和方法(成员函数)。我们使用引用,就是与其中的字段进行互动。
也有一些字段是某一对象的引用,此时也需要使用new关键字对这一引用进行初始化。
一般来说,对象都会单独保存其字段,不同对象的字段之间不会共享:
class DataOnly{
int i;
double d;
boolean b;
}
上述代码定义的类只有数据字段,当我们为这一类创建了一个对象后,就可以通过对象成员为字段赋值(引用与字段之间使用“.”进行连接):
//创建对象
DataOnly data = new DataOnly();
//通过对象的引用名进行字段赋值
data.i = 10;
data.d = 1.1;
data.b = false;
类中的默认值
若一个类的某一字段是基本类型,那么这一字段就会默认被初始化(这一机制只适用于当变量作为类的成员存在时)。这种默认值不一定合理,所以最好的方式还是显式地初始化。
除此之外,局部变量并不会被这样初始化,此时需要程序员为其赋值。若没有进行赋值,Java就会进行报错。
方法、参数和返回值
在C和C++中,“函数”用于表示子程序。但Java没有这一概念,取而代之的是“方法”,即“做某件事的方式”。方法有四个最基础的部分:方法名、参数、返回值和方法体,例如:
ReturnType methodName(/* 参数列表 */){
// 方法体
}
其中:
- 返回值:表示当调用该方法时,该方法生成的值是什么类型;
- 参数列表:提供需要传递给方法的信息(类型和名称);
- 方法名和参数列表共同构成了“方法签名”(该方法的唯一标识符)。
在Java中,方法只能作为类的一部分存在,且只能通过对象进行调用。
调用对象方法和调用字段类似,都是通过“.”进行,不同的是调用方法时还需要写明参数列表:
objectReference.methodName(arg1, arg2, arg3);
参数列表
参数列表负责描述传递给方法的信息,和Java的其他部分一样,这些被传递的信息也是对象。因此,在进行传参的时候,也会需要描述这些对象的类型和对象名。因为实际被操作的是对象的引用,所以引用的类型就必须正确——传入的对象类型必须与方法定义的参数类型一致。
int f(String s){
return s.length * 2;
}
return关键字使方法结束,并且将方法产生的值进行返回,该值的类型也必须与返回值的类型相一致。
编写程序
名称可见性
为了方便区分不同模块的相同名称,C++引入了命名空间的概念。而Java则另辟蹊径,设计者将互联网域名反转使用(域名是唯一的),以此保证冲突不会发生。举个例子:若域名是xxx.com,那么abc库的名称就是com.xxx.utility.abc。其中的“.”用来分隔域名(其中,utility指Java的实用工具包)。
通过上述的这种方式,Java为文件中的每一个类生成了唯一对应的标识符,而由于一些历史原因,包名都是小写字母。
通过关联命名空间和反转的URL生成文件路径存在一个缺点,那就是冗长的文件夹路径。还是上面的例子:若我需要使用com.xxx.utility.abc这一命名空间,我就需要创建com和xxx的空文件夹。在一些更大的工程中,这些路径也被用于保存一些表明文件夹内容的数据,而为了管理这些套娃式的代码,使用好的IDE是一个不错的选择。
使用组件
编译器无法自动找到存放在其他文件中的类,最简单的理由是一个类可能存在许多不同的定义。为此,就需要使用import关键字:import关键字的作用就是通知编译器导入一个指定位置的包(即放置了各种类的库)。
若使用的是编译器自带的各种Java标准库组件,就不需要考虑那些冗长的域名,而可以这样直接使用:
import java.util.ArrayList; //使用位于util库的ArrayList类
或者使用通配符“*”引入util库中的所有类:
import java.util.*;
但每一个被用到的类最好应该被单独引入。
static关键字
若我们需要使用一个对象,一般需要先使用new关键字为对象分配内存,然后才能使用其的方法。但这并没有办法很好地处理下面两种情况:
- 需要通过一块共享空间保存特定字段;
- 即便没有生成对应的对象,也希望可以使用其中的某些方法。
为此,Java使用了static关键字,带有static的方法或者字段不会依赖于任何对象(当然,我们无法通过static对象调用非static成员或方法)。这些只服务于类的数据或方法也被称为“类数据”和“类方法”。创建并初始化一个static字段的例子如下:
class StaticTest{
static int i = 0;
}
就算存在两个StaticTest的对象,StaticTest.i也只会占据一块内存空间。这意味着字段i会被两个对象共享。
通过类名和引用名都可以调用static成员,例如:
//使用类名进行static字段的调用
StaticTest.i++;
//使用引用名进行static字段的调用
StaticTest t = new StaticTest();
t.i++;
一般更推荐使用类名调用static方法。
同理,也可以这样调用static方法,例如:
class Test {
static void f() {
StaticTest.i++;
}
}
//通过类名调用static方法
Test.f();
static关键字的使用会改变数据生成的方式:static字段基于类进行创建,而非static字段是基于对象创建的。
Java程序
这是一个完整的Java程序,该程序打印了一串字符串,和通过Java标准库的Date类生成的日期:
import java.util.*; //使用import引入额外的类
public class HelloWorld {
public static void main(String[] args) {
System.out.printf("Hello world, it's: "); //打印字符串
System.out.println(new Date()); //打印Date类生成的日期
}
}
其中的out字段就是一个static PrintStream对象,因此它无需创建就可以直接使用。
该代码的生成结果如下:
每一个Java文件都会自动导入一个特定的库,即java.lang。而除此之外的类,就需要使用import关键字进行引入。上述程序中使用到的Date类就存在于util库中,因此我们需要在程序的开头进行util库的引入。
注意:文件中必须要有一个与该文件同名的类。若这个文件是一个可以单独运行的文件,与文件同名的类中还需要有一个入口方法main(),其格式和返回值如下:
public static void main(String[] args) { //...
其中,public关键字表示该方法可以被外部程序所调用。main()的参数是一个String对象的数组,args参数是被要求强制传递的。
编译和运行
若需要编译之前编写的程序(HelloWorld.java),首先需要打开该程序所在的目录,然后通过命令行输入:
javac HelloWorld.java
若编译没有报错,则会生成一个HelloWorld.class文件,此时可以继续输入:
java HelloWorld
通过这行命令就可以将程序的结果输出到控制台了。
编程风格(驼峰式命名法)
“驼峰式命名法”要求:
- 类名:首字母要大写,若是类名包含多个词,则汇总所有词,并且每一个词的首字母都要大写,例如:
class AllTheColorOfTheRainbow{ //...
- 其他内容(方法、字段和对象名等):首字母小写,其他的地方和类名一样,例如:
class AllTheColorOfTheRainbow{ int anTnterRepresentingColors; void changeTheHueOfTheColor(int newHue) { //... } //... }