Free Will

Java学习笔记(1):语法基础

Java是完全面向对象的语言。通过虚拟机的运行机制,实现“跨平台”的理念。一次编译,永久使用。

先来看一个 HelloWorld.java 程序。这个程序在屏幕上打印出一串字符”Hello World!”:

1
2
3
4
5
6
7
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}

程序中包括Java的一些基本特征:

  • 类(class):上面程序定义了一个 类 HelloWorld,该类的名字与.java文件的名字相同。
  • 方法(method):类的内部定义了该类的一个 方法 main。
  • 语句(statement):真正的“打印”功能由一个语句实现,即: System.out.println(“Hello World!”);

下面两点有关Java的书写方式:

  • Java中的语句要以 ; 结尾 (与C/C++相同)。
  • 用花括号 {} 来整合语句,形成程序块。通过程序块,我们可以知道程序的不同部分的范围,比如类从哪里开始,到哪里结束。

一、编译与运行

Java程序要经过编译器编译才能执行。在Linux或Mac下,可以下载安装 Java JDK

使用 javac 来编译。在命令行中输入下面语句 编译 :

1
$javac HelloWorld.java

当前路径下,将有一个名为HelloWorld.class的文件生成。
使用 java 命令来 运行 。

1
$java HelloWorld

Java会搜寻该类中的main方法,并执行。

二、变量

计算机语言通常需要在内存中存放数据,比如C语言中的变量,Java也有类似的变量。Java和C语言都是静态类型的语言。在使用变量之前,要声明变量的类型。

变量(variable) 占据一定的内存空间。不同类型的变量占据不同的大小。Java中的变量类型如下:

变量类型 存储大小 例值 注释
byte 1byte 3 字节
int 4bytes 3 整数
short 2bytes 3 短整数
long 8bytes 3 长整数
float 4bytes 1.2 单精度浮点数
double 8bytes 1.2 双精度浮点数
char 2bytes ‘a’ 字符
boolean 1bit true 布尔值

在Java中,变量需要先 声明(declare)才能使用。在声明中,我说明变量的类型,赋予变量以特别名字,以便在后面的程序中调用它。你可以在程序中的任意位置声明变量。 比如:

1
2
3
4
5
6
7
8
9
10
public class Test
{
public static void main(String[] args)
{
System.out.println("Declare in the middle:");
int a;
a = 5;
System.out.println(a); // print an integer
}
}

上面a是变量名。可以在声明变量的同时,给变量赋值,比如 int a = 5;

  • “变量”的概念实际上来自于面向过程的编程语言。在Java中,所谓的变量实际上是 “基本类型” (premitive type) 。我们将在类的讲解中更多深入。

上面的程序还可以看到,Java中,可用 // 引领注释。

二、数组

Java中有 数组(array) 。数组包含相同类型的多个数据。我用下面方法来声明一个整数数组:

1
int[] a;

在声明数组时,数组所需的空间并没有真正分配给数组。我可以在声明的同时,用new来创建数组所需空间:

1
int[] a = new int[100];

这里创建了可以容纳100个整数的数组。相应的内存分配也完成了。

我还可以在声明的同时,给数组赋值。数组的大小也同时确定。

1
int[] a = new int[] {1, 3, 5, 7, 9};

2.1 数组初始化

你可以在声明数组的同时进行初始化(静态初始化),也可以在声明以后进行初始化(动态初始化)。例如:

1
2
3
4
5
6
7
8
9
10
// 静态初始化
// 静态初始化的同时就为数组元素分配空间并赋值
int intArray[] = {1,2,3,4};
String stringArray[] = {"程序员", "http://www.baidu.com", "一切编程语言都是纸老虎"};
// 动态初始化
float floatArray[] = new float[3];
floatArray[0] = 1.0f;
floatArray[1] = 132.63f;
floatArray[2] = 100F;

2.2 数组引用

可以通过下标来引用数组:

1
arrayName[index];

index从0开始。与C、C++不同,Java对数组元素要进行越界检查以保证安全性。

每个数组都有一个length属性来指明它的长度,例如 intArray.length 指明数组 intArray 的长度。

其他类型的数组与整数数组相似。

2.3 数组的遍历

实际开发中,经常需要遍历数组以获取数组中的每一个元素。最容易想到的方法是for循环,例如:

1
2
3
4
int arrayDemo[] = {1, 2, 4, 7, 9, 192, 100};
for(int i=0,len=arrayDemo.length; i<len; i++){
System.out.println(arrayDemo[i] + ", ");
}

不过,Java提供了”增强版“的for循环,专门用来遍历数组,语法为:

1
2
3
for( arrayType varName: arrayName ){
// Some Code
}

arrayType 为数组类型(也是数组元素的类型);varName 是用来保存当前元素的变量,每次循环它的值都会改变;arrayName 为数组名称。

每循环一次,就会获取数组中下一个元素的值,保存到 varName 变量,直到数组结束。即,第一次循环 varName 的值为第0个元素,第二次循环为第1个元素……例如:

1
2
3
4
int arrayDemo[] = {1, 2, 4, 7, 9, 192, 100};
for(int x: arrayDemo){
System.out.println(x + ", ");
}

这种增强版的for循环也被称为”foreach循环“,它是普通for循环语句的特殊简化版。所有的foreach循环都可以被改写成for循环。

但是,如果你希望使用数组的索引,那么增强版的 for 循环无法做到。

2.4 二维数组

二维数组的声明、初始化和引用与一维数组相似:

1
2
3
4
5
6
int intArray[ ][ ] = { {1,2}, {2,3}, {4,5} };
int a[ ][ ] = new int[2][3];
a[0][0] = 12;
a[0][1] = 34;
// ......
a[1][2] = 93;

Java语言中,由于把二维数组看作是数组的数组,数组空间不是连续分配的,所以不要求二维数组每一维的大小相同。例如:

1
2
3
4
int intArray[ ][ ] = { {1,2}, {2,3}, {3,4,5} };
int a[ ][ ] = new int[2][ ];
a[0] = new int[3];
a[1] = new int[5];

【示例】通过二维数组计算两个矩阵的乘积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Demo {
public static void main(String[] args){
// 第一个矩阵(动态初始化一个二维数组)
int a[][] = new int[2][3];
// 第二个矩阵(静态初始化一个二维数组)
int b[][] = { {1,5,2,8}, {5,9,10,-3}, {2,7,-5,-18} };
// 结果矩阵
int c[][] = new int[2][4];
// 初始化第一个矩阵
for(int i=0; i<2; i++)
for(int j=0; j<3 ;j++)
a[i][j] = (i+1) * (j+2);
// 计算矩阵乘积
for (int i=0; i<2; i++){
for (int j=0; j<4; j++){
c[i][j]=0;
for(int k=0; k<3; k++)
c[i][j] += a[i][k] * b[k][j];
}
}
// 输出结算结果
for(int i=0; i<2; i++){
for (int j=0; j<4; j++)
System.out.printf("%-5d", c[i][j]);
System.out.println();
}
}
}
1
2
3
运行结果:
25 65 14 -65
50 130 28 -130

几点说明

  • 上面讲的是静态数组。静态数组一旦被声明,它的容量就固定了,不容改变。所以在声明数组时,一定要考虑数组的最大容量,防止容量不够的现象。
  • 如果想在运行程序时改变容量,就需要用到数组列表(ArrayList,也称动态数组)或向量(Vector)。
  • 正是由于静态数组容量固定的缺点,实际开发中使用频率不高,被 ArrayList 或 Vector 代替,因为实际开发中经常需要向数组中添加或删除元素,而它的容量不好预估。

三、表达式

表达式 是变量、常量和运算符的组合,它表示一个数据。 1 + 1 是常见的表达式。再比如:

1
2
3
4
5
6
7
8
9
10
public class Test
{
public static void main(String[] args)
{
System.out.println("Declare in the middle:");
int a;
a = 5 + 1;
System.out.println(a); // print an integer
}
}

上面的5 + 1也是一个表达式,等于6。

3.1 数学表达式

数学运算,结果为一个数值

1 + 2 加法
4 - 3.4 减法
7 * 1.5 乘法
3.5 / 7 除法
7 % 2 求余数

3.2 关系表达式

判断表达式是否成立。即一个boolean值,真假

a > 4.2 大于
3.4 >= b 大于等于
1.5 < 9 小于
6 <= 1 小于等于
2 == 2 等于
2 != 2 不等于

3.3 布林表达式

两个boolean值的与、或、非的逻辑关系

true && false and
(3 > 1) (2 == 1) or
! true not

3.4 位运算

对整数的二进制形式逐位进行逻辑运算,得到一个整数

& and
or
^ xor
~ not
5 << 3 0b101 left shift 3 bits
6 >> 1 0b110 right shift 1 bit

还有下列在C中常见的运算符,我会在用到的时候进一步解释:

m ++ 变量m加1
n — 变量n减1
condition ? x1 : x2 condition为一个boolean值。根据condition,取x1或x2的值

四、控制结构

Java中控制结构(control flow)的语法与C类似。它们都使用{}来表达隶属关系。

4.1 选择(if)

condition是一个表示真假值的表达式。statements;是语句。

1
2
3
4
5
6
7
8
9
10
11
12
if (conditon1) {
statements;
...
}
else if (condition2) {
statements;
...
}
else {
statements;
...
}

4.2 循环(while)

1
2
3
4
5
while (condition) {
statements;
}

4.2 循环(do…while)

1
2
3
do {
statements;
}while(condition); // 注意结尾的;

4.3 循环(for)

1
2
3
for (initial; condition; update) {
statements;
}

4.4 跳出或跳出循环

在循环中,可以使用

1
2
break; // 跳出循环
continue; // 直接进入下一循环

4.5 选择(switch)

1
2
3
4
5
6
7
8
9
10
11
12
switch(expression) {
case 1:
statements;
break;
case 2:
statements;
break;
...
default:
statements;
break;
}

五、字符串

从表面上看,字符串就是双引号之间的数据,例如“java”等。在Java中,可以使用下面的方法定义字符串:
String stringName = “string content”;

1
String webName = "java学习";

字符串可以通过“+”连接,基本数据类型与字符串进行“+”操作一般也会自动转换为字符串,例如:

1
2
3
4
5
6
7
8
9
10
public class Demo {
public static void main(String[] args){
String stuName = "小明";
int stuAge = 17;
float stuScore = 92.5f;
String info = stuName + "的年龄是 " + stuAge + ",成绩是 " + stuScore;
System.out.println(info);
}
}

String字符串与数组有一个共同点,就是它们被初始化后,长度是不变的,并且内容也不变。如果要改变它的值,就会产生一个新的字符串,如下所示:

1
2
String str = "Hello ";
str += "World!";

这个赋值表达式看起来有点像简单的接龙,在str后面直接加上一个“World!”字符串,形成最后的字符串“Hello World!”。其运行原理是这样的:程序首先产生了str1字符串,并在内存中申请了一段空间。此时要追加新的字符串是不可能的,因为字符串被初始化后,长度是固定的。如果要改变它,只有放弃原来的空间,重新申请能够容纳“Hello World!”字符串的内存空间,然后将“Hello World!”字符串放到内存中。

实际上,String 是java.lang包下的一个类,按照标准的面向对象的语法,其格式应该为:

1
String stringName = new String("string content");

例如

1
String url = new String("http://www.baidu.com");

但是由于String特别常用,所以Java提供了一种简化的语法。

使用简化语法的另外一个原因是,按照标准的面向对象的语法,在内存使用上存在比较大的浪费。例如String str = new String(“abc”);实际上创建了两个String对象,一个是”abc”对象,存储在常量空间中,一个是使用new关键字为对象str申请的空间。

5.1 常用的String对象方法

5.1.2 length()方法

length() 返回字符串的长度,例如:

1
2
3
4
String str1 = "微学苑";
String str2 = "weixueyuan";
System.out.println("The lenght of str1 is " + str1.length());
System.out.println("The lenght of str2 is " + str2.length());

输出结果为:

1
2
The lenght of str1 is 3
The lenght of str2 is 10

5.1.2 charAt()方法

charAt() 方法的作用是按照索引值获得字符串中的指定字符。Java规定,字符串中第一个字符的索引值是0,第二个字符的索引值是1,依次类推。例如:

1
2
String str = "123456789";
System.out.println(str.charAt(0) + " " + str.charAt(5) + " " + str.charAt(8))

输出结果为:

1
1 6 9

5.1.3 contain()方法

contains() 方法用来检测字符串是否包含某个子串,例如:

1
2
String str = "baidu";
System.out.println(str.contains("bai"));

输出结果:

1
true

5.1.4 replace()方法

字符串替换,用来替换字符串中所有指定的子串,例如:

1
2
3
4
ring str1 = "The url of baidu is www.google.com!";
String str2 = str1.replace("baidu", "google");
System.out.println(str1);
System.out.println(str2);

输出结果:

1
2
The url of baidu is www.google.com!
The url of google is www.google.com!

注意:replace() 方法不会改变原来的字符串,而是生成一个新的字符串。

5.1.5 split()方法

以指定字符串作为分隔符,对当前字符串进行分割,分割的结果是一个数组,例如:

1
2
3
4
5
6
7
8
import java.util.*;
public class Demo {
public static void main(String[] args){
String str = "wei_xue_yuan_is_good";
String strArr[] = str.split("_");
System.out.println(Arrays.toString(strArr));
}
}

运行结果:

1
[wei, xue, yuan, is, good]

以上仅仅列举了几个常用的String对象的方法,更多方法和详细解释请参考API文档。

5.2 StringBuffer与StringBuider

String 的值是不可变的,每次对String的操作都会生成新的String对象,不仅效率低,而且耗费大量内存空间。

StringBuffer类和String类一样,也用来表示字符串,但是StringBuffer的内部实现方式和String不同,在进行字符串处理时,不生成新的对象,在内存使用上要优于String。

StringBuffer 默认分配16字节长度的缓冲区,当字符串超过该大小时,会自动增加缓冲区长度,而不是生成新的对象。

StringBuffer不像String,只能通过 new 来创建对象,不支持简写方式,例如:

1
2
3
4
StringBuffer str1 = new StringBuffer(); // 分配16个字节长度的缓冲区
StringBuffer str2 = =new StringBuffer(512); // 分配512个字节长度的缓冲区
// 在缓冲区中存放了字符串,并在后面预留了16个字节长度的空缓冲区
StringBuffer str3 = new StringBuffer("www.baidu.com");

5.2.1 StringBuffer类的主要方法

StringBuffer类中的方法主要偏重于对于字符串的操作,例如追加、插入和删除等,这个也是StringBuffer类和String类的主要区别。实际开发中,如果需要对一个字符串进行频繁的修改,建议使用 StringBuffer。

  • 1) append() 方法

append() 方法用于向当前字符串的末尾追加内容,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也发生改变,例如:

1
2
StringBuffer str = new StringBuffer(“biancheng100”);
str.append(true);

则对象str的值将变成”biancheng100true”。注意是str指向的内容变了,不是str的指向变了。

字符串的”+“操作实际上也是先创建一个StringBuffer对象,然后调用append()方法将字符串片段拼接起来,最后调用toString()方法转换为字符串。

这样看来,String的连接操作就比StringBuffer多出了一些附加操作,效率上必然会打折扣。

但是,对于长度较小的字符串,”+“操作更加直观,更具可读性,有些时候可以稍微牺牲一下效率。

  • 2) deleteCharAt()

deleteCharAt() 方法用来删除指定位置的字符,并将剩余的字符形成新的字符串。例如:

1
2
StringBuffer str = new StringBuffer("abcdef");
str. deleteCharAt(3);

该代码将会删除索引值为3的字符,即”d“字符。

你也可以通过delete()方法一次性删除多个字符,例如:

1
2
StingBuffer str = new StringBuffer("abcdef");
str.delete(1, 4);

该代码会删除索引值为1~4之间的字符,包括索引值1,但不包括4。

  • 3) insert()方法

insert() 用来在指定位置插入字符串,可以认为是append()的升级版。例如:

1
2
StringBuffer str = new StringBuffer("abcdef");
str.insert(3, "xyz");

最后str所指向的字符串为 abcdxyzef。

  • 4) setCharAt() 方法

setCharAt() 方法用来修改指定位置的字符。例如:

1
2
StringBuffer str = new StringBuffer("abcdef");
str.setCharAt(3, 'z');

该代码将把索引值为3的字符修改为 z,最后str所指向的字符串为 abczef。

以上仅仅是部分常用方法的简单说明,更多方法和解释请查阅API文档。

5.2.2 String和StringBuffer的效率对比

为了更加明显地看出它们的执行效率,下面的代码,将26个英文字母加了10000次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Demo {
public static void main(String[] args){
String fragment = "abcdefghijklmnopqrstuvwxyz";
int times = 10000;
// 通过String对象
long timeStart1 = System.currentTimeMillis();
String str1 = "";
for (int i=0; i<times; i++) {
str1 += fragment;
}
long timeEnd1 = System.currentTimeMillis();
System.out.println("String: " + (timeEnd1 - timeStart1) + "ms");
// 通过StringBuffer
long timeStart2 = System.currentTimeMillis();
StringBuffer str2 = new StringBuffer();
for (int i=0; i<times; i++) {
str2.append(fragment);
}
long timeEnd2 = System.currentTimeMillis();
System.out.println("StringBuffer: " + (timeEnd2 - timeStart2) + "ms");
}
}

运行结果:

1
2
String: 5287ms
StringBuffer: 3ms

结论很明显,StringBuffer的执行效率比String快上千倍,这个差异随着叠加次数的增加越来越明显,当叠加次数达到30000次的时候,运行结果为:

1
2
String: 35923ms
StringBuffer: 8ms

所以,强烈建议在涉及大量字符串操作时使用StringBuffer。

5.2.3 StringBuilder类

StringBuilder类和StringBuffer类功能基本相似,方法也差不多,主要区别在于StringBuffer类的方法是多线程安全的,而StringBuilder不是线程安全的,相比而言,StringBuilder类会略微快一点。

StringBuffer、StringBuilder、String中都实现了CharSequence接口。CharSequence是一个定义字符串操作的接口,它只包括length()、charAt(int index)、subSequence(int start, int end) 这几个API。

StringBuffer、StringBuilder、String对CharSequence接口的实现过程不一样。String直接实现了CharSequence接口;StringBuilder 和 StringBuffer都是可变的字符序列,它们都继承于AbstractStringBuilder,实现了CharSequence接口。

总结一下:

  • 线程安全:
    • StringBuffer:线程安全
    • StringBuilder:线程不安全
  • 速度:

    • 一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。
  • 使用环境:

    • 操作少量的数据使用 String;
    • 单线程操作大量数据使用 StringBuilder;
    • 多线程操作大量数据使用 StringBuffer。


应统联盟


连接十万名应统专业同学


阿药算法


打通算法面试任督二脉