目录

变量

定义变量必须先约定用来存那种类型的值,存数字就用 int。int 能存的值还有个范围大约是正负 21 亿,超出范围就无法处理。

在这里你就发现 Java 与 Python 定义变量语法区别,前者会让你提前规划好变量存什么类型数据,其中数据能存多大都要想到。

看到第三行其中 iny y 叫声明变量,= 是赋值运算符讲 2 这个字面值赋值给变量 y。变量名在声明时(标识符)不能用与数字开头,也不能用像 publicint 等关键字,作为标识符。而且标识符和关键字都是区分大小写。

public class HelloWorld {
    public static void main(String [] args) {
        int x;
        x = 1;

        int y = 2;
    }
}

Java 程序结构:

  1. 表达式(expression),像 int x; 就是声明一个 x 变量,除去分号剩余都是表达式,表达式一般用于运算。而 x = 1 其中的 1 是字面值(Literal Value)。
  2. 语句(statement),一个带分号的表达式就是语句,像单个 ; 就是一个空的表达式,语句是由多个表达式构成,像是 int x = 1 + 2;,就是由变量 x 和 1 + 2 这两个表达式构成。
  3. 代码块(code blocks),带大括号的就是代码块,像 {} 就是一个空的代码块,像我们敲的 main 方法就是一个代码块。

数据类型

整数

  • byte,占用 1 个 byte 内存空间,值域(取值范围) -128 ~ 127。

  • short,占用 2 个 byte 内存空间,值域(取值范围) -32768 ~ 32767。

  • int,占用 4 个 byte 内存空间,值域 -2147483648 ~ 2147483647(-2^31^~-2^31-1^),在 Java 中整数默认值是 int。

  • long,占用 8 个 byte 内存空间,值域 -9223372036854775808 ~ 9223372036854775807。

当声明一个值使用 long shortValue = 99,因为是整数类型嘛,所以是默认 int 型,并不是使用了 long 关键词声明就成为了 long 类型,只有数值超出 int 范围并且加了 L 才是正确声明 long,比如 long shortValue = 999999999L,在数字最后一位加上 L 此时才是 long,不加 L 超出范围则会报错,标识使用上不推荐使用小写字母 l,不好和数字 1 区分。

浮点数

  • float,占用 4 个 byte 有一个超大的范围值 ±340282346638528859811704183484516925440。计算机在处理浮点数时会有精度控制,最直观的展现是无限循环小数计算到一定位数停止。

  • double,占用 8 个 byte 精度是 float 两倍。在 Java 中浮点数默认值是 double。

因为浮点数默认值是 double 类型,所以在声明 float 类型的浮点数时要加个 f 以区分 double,比如 float floatValue = 1.1f,必须要加 f,字母不区分大小写,大 f 小 F 随意。

浮点数精度的问题是所有编程语言都无法避免的,参考 FAQ 01章-为什么计算机里的浮点数不精确.md 和《程序是怎样运行的》第三章

上面数值类型值域为什么都是从负数到正数?一个 byte 有 8 位 bit,第 1 位 bit 是符号位必须用来标识你存的这个数是整数还是负数,0 代表这个数为整数,1 则是负数。

布尔和字符

  • boolean,占用 1 个 byte,值域 true/false。在 Python 里是大写开头。
  • char,占用 2 个 byte,值域是任意单个字符。

布尔很简单只有两个状态,true/false。

boolean status1 = true;
boolean status0 = false;

System.out.println(status1);
System.out.println(status0);

Java 中如果要表示单个字符,就可以用单引号将字符引起来 'A',当你在里面输入多个字符就会报错。char 可以和 int 类型相互转换,一个字符对应着 ASCII,这个 ASCII 其实就是数字它俩是映射关系。

char x = 'a' + 1;
int z = 65;
char c = (char) z;

System.out.println(x);
System.out.println(c);

如果使用很多字符必须使用双引号包裹起来,形成字符串,其实就是将一个个字符串起来嘛。字符串在 Java 中不是基础数据类型的一部分,所以 String 不是 Java 保留字。在使用方面和其他类型一样,前面使用 String 来声明类型,例如 String user = "用户名";

运算符

赋值运算符

  • =,赋值运算符。

计算运算符

  • +-*/,运算后不会对原数值进行更改。
  • %,取模(取余数),10 % 5,用的少。

整数在做除法不会得出小数,两个相同数据类型做运算后不会改变原有类型,比如:

int a = 10;
int b = 3;
float c = 3.0F;

System.out.println(a / b); // 两个 int 结果也是 int
System.out.println(a / c); // 结果才是 float

做数学运算时也是按照先乘除后加减的优先级进行。

比较运算符

比较结果返回 true 或 false。

  • ==

  • !=

  • >

  • >=

  • <

  • <=

布尔运算符

  • !,not(非)运算符,常常用于取反,!true 得到 false,!false 得到 true。
  • &,and(且)运算符,两个布尔值运算,只要有一个 false,最终返回 false。
  • |,or(或)运算符,只要有一个 true 最终返回 true。
  • &&,andand(且且)运算符。
  • ||,oror(或或)运算符。

单个 |&,是不会使用短路运算的,就算左边操作数运算完右边的操作数也要计算,就是无论如何右边操作数都会进行运算。

在大多数程序都是 &&|| 用这种运算符,它会进行短路运算,只要左边操作数条件确定,右边不参与运算。

比如 && 左为 false,直接停止运算得到结果 false,如果是 true 则需要 判断右边操作数,为 true 则返回 true。而 || 呢 只要左操作数为 true 直接得到 true 不会再和右操作数进行运算,只有左操作数是 false 才需要运算右边操作数是否为 false。

位运算符

Java 字面值以 0 开头的是八进制,0x 开头是十六进制,x 大小写不区分,小写用的多。

int e = 0xFF;
int f = 077;
System.out.println(e); // 255
System.out.println(f); // 63

将数都变为二进制,一位一位 bit 做运算,实际中用的少。

  • &,做按位与(AND)操作,和 Boolean 一样 两操作数 都是 1 得 1,只要有一个 0 结果就为 0。

      11111111
      00000000
      ────────
      10000000
  • | ,做按位或(OR)操作,只要有一个 1 则结果为 1。只有两个操作数为 0 结果才是 0。

      11111100
      11101101
      ────────
      11111101
  • ^,做按位异或(XOR)操作,两个操作数相同结果为 0,不同结果为 1。

      11111100
      11101101
      ────────
      00010001
  • ~,单操作数,对每个 bit 进行取反,包括符号位。0 为 1,1 为 0。

      11111111
      ────────
      00000000

位移运算符

补充位移运算作为乘除运算。

和位运算符一样将数值转为 bit 做左右移动运算。

<< 左移,将所有 bit 位整体移动 n 位,在末位补 0。

比如 3 << 1,先将 3 转为二进制,在进行位移,一共 8 位溢出的数字会被丢弃。一直左移,符号位变成 1 那整个结果就为负数。

   0000 0011 // 数字 3
 0 0000 0110 // 整体向左移 1 位,在末位补 0。得到数字 6

一个有趣的事实是左移 1 bit 整体数值结果是原数值两倍,左移 2 bit 就是 4 倍,以此类推。这样来看左移也能作为乘法运算呢。不过为什么整数 3 并没得到 1.5 呢?因为是整数的缘故,用浮点数就不能进行位移运算

>> 右移,除符号位外所有 bit 整体右移(也叫带符号右移),最后在符号位后补数,整数补 0,负数补 1。

一样拿 3 举例,向右移 1 bit 得到 十进制 1。

0000 0011   // 数字 3
0000 0001 1 // 所有 bit 整体向右移 1 位,由于 3 是整数所以在符号位空出来的 bit 后面补 0。末尾的 1 多出来会被丢弃。

跟左移一样,右移 1 bit 整体数值是原数值的 2/1,相当于除 2,右移也可以用于除法运算。

>>> 所有 bit 整体右移,包括符号位(无符号右移),移动后上符号位后面补 0。

数字 13 向右移 1 bit,得到十进制 6。

0000 1101   // 数字 13
0000 0110 1 // 所有 bit 整体向右移 1 位,在符号位后面补 0。

参考资料:

  • 《程序是怎样跑起来的》2.3-2.5 节

计算并赋值运算符

所有计算类运算符都可使用简化操作,比如变量 a 要加 1,原始写法是 a = a + 1,新写法是 a += 1,这种操作叫计算并赋值运算符。

  • +=
  • -=
  • *=
  • /=
  • %=
  • &=
  • ^=
  • |=
  • <<=
  • >>=
  • >>>=

自增自减运算符

  • ++var,对 var 变量进行前加加运算
  • var++,对 var 变量后加加运算
  • --var,对 var 变量前减减运算
  • var--,对 var 变量后减减运算

a++ 是先操作完 a 再进行 ++,相当于 a +=1,最终变量 a 会改变加 1。比如 "testNumber" + a++; 则是先执行字符串先和变量 a 拼接,操作完成后再进行 ++。

byte a = 1;
System.out.println("字符串先进行拼接操作并输出打印字符串,下一步再对a进行++操作:" + a++);
System.out.println("变量a确实是操作后加1:" + a);

++a 是先对 a 进行 ++,再使用 a。

byte a = 1
System.out.println("先进行变量a先++,最后进行拼接字符串操作:" + ++a);

自减操作也是一样的原理。

运算符优先级

优先级从高到低,从左到右。算数运算符 > 比较运算符 > 布尔运算符 > 赋值运算符。

  • ()
  • !~++--
  • */%
  • <<>>>>>
  • +-
  • >>=<<=
  • ==,比较运算必须让前面算数运算得出结果才能比较鸭,还没算完就比较会出先值不准确的情况。
  • !=
  • &&&|||,得通过前面的比较运算符先得出布尔值再进行布尔运算
  • =,从右往左计算嘛,优先级最低可以理解。

数据类型自动转换

在做数字运算时或赋值时,低精度会先自动转换为高精度类型(称隐性/自动转换)。

精度从大到小排列 1:double > float > long > int > short > byte
精度从大到小排列 2:double > float > long > int > char

byte y = -127;
long z = 999999999;
System.out.println(z + y); // 最终得到 long 类型的数值 999999872

double x = -127;
long p = 999999999;
System.out.println(x + p); // 最终得到 double 类型的数值 9.99999872E8

double v = 127; // int 自动转为高精度 double。
int asz = 'a'; // char 自动转换为高精度 int。

byte num1 = 123;
short num2 = num1; // 证明 byte 能转为 short。

自动类型转换有个例外,就是 byte、short 不能与 char 进行转换,见 9-12 行。而这三种类型在做运算时会提升为 int,最终结果就是 int 类型,见 5-7 行。

byte num1 = 10;
short num2 = 20;
char str = 'A';

int result1 = num1 + str; // 最终得到 int 75
int result2 = num2 + str; // 最终得到 int 85
int result3 = num1 + num2; // 最终得到 int 30

str = num1; // java: 不兼容的类型: 从byte转换到char可能会有损失
num1 = str; // java: 不兼容的类型: 从char转换到byte可能会有损失
short zz = str; // java: 不兼容的类型: 从char转换到short可能会有损失
str = num2; // java: 不兼容的类型: 从short转换到char可能会有损失

从高精度转为低精度可能会有一个数据丢失的问题,所以需要强制类型转换,具体操作时在数值前写一个小括号,括号里填类型,(type) number。所以这就需要你考虑到要操作的数值一个范围,去选择合适的数据类型来存储,比如有两个相同类型的变量计算得到的结果不能超出此数据类型能够存储的阈值,像两个 int 做运算得到超出 int 能够存储的最大值则直接截断数据,这样会造成数据丢失。

long y = (short) 999999999;
long z = (byte) 999999999;
System.out.println(y); // -1
System.out.println(z); // -13825

流程控制

条件分支

if

最简化,只有 if boolean 是 true 就执行代码块内容。当然这个 boolean 可以是比较运算符计算后的结果,也可以是变量,只要最终值是 boolean 就行。

if (boolean) {
    statement;
}

也可以加上 else。当 boolean 是 false 就执行 else 代码块内容。

if (boolean) {
    statement;
} else {
    statement;
}

进一步。

if (boolean) {
    statement;
} else if (boolean) {
    statement;
} else {
     statement;
}

如果 block 里只有一行语句可以省略花括号,多行则报错。不推荐使用此方式。

if (boolean)
    statement;
else
    statement;

也可以把 else 花括号单独省略

if (boolean) {
    statement;
} else
    statement;

上面的操作可以由三元操作符进一步简化,由问号和冒号分隔的三个表达式,condition 是操作后的结果相当于 if (boolean),只要为 true 就执行冒号前面表达式 expression1,false 执行 expression2。

condition ? expression1 : expression2

什么时候用呢?当一个判断一眼就能看懂是在做什么时使用,如果逻辑复杂最好还是使用传统 if 判断。

switch

switch 条件是必须为 int 型(在 JRE 11 中可以使用字符串当作条件),其中 case 匹配成功会执行里面的语句,语句中如果没有 break; 会一下向下执行其他 case 内容直到遇见 break;,当所有 case 匹配失败则执行 default 中的语句,这个 default 可以放在任意位置只要遇到就执行,也可以不写。

int n = 3;
String str = "数字是:";

switch (n) {
    case 1:
        str += "一";
        break;
    case 2:
        str += "二";
        break;
    default:
        System.out.println('出错了,都没匹配上。');
}

循环

for

for 循环,可以控制循环次数。第一次循环执行顺序是,首先初始化变量,接着循环体条件表达式会判断初始变量有没满足条件,满足执行 block 中的语句,执行完会执行循环体后语句,第二次则从循环体条件表达式开始判断初始变量是否满足,满足还是执行 block 内容,接着执行循环体语句,第三、四、N...都是如此,直到不满足条件。

for (初始变量; 循环体条件表达式; 循环体后语句) {
    statement;
}

此外循环体后语句可以有多个,不过不推荐使用,影响程序可读性。

for (初始变量; 循环体条件表达式; 循环体后语句, 循环体后语句2) {
    statement;
}

在所有循环中可以使用 break 直接中断本次循环,continue 跳出本次循环重新执行一次,这和其他语言一致。有意思的一个点是可以完全通过定义标签让 break 语句跳出嵌套多层的循环。

public class brekTest {
    public static void main(String[] args) {
        breakName: // 标签放置在循环外用于指定跳出哪个循环,标签后面需要跟上冒号。
        while(true) {
            System.out.println("第一层循环");
            while(true) {
                System.out.println("第二层循环");
                break breakName; // break 直接跳出升天。
            }
        }
        System.out.println("已经退出循环");
    }
}

不加则死循环

public class brekTest {
    public static void main(String[] args) {
        breakName:
        while(true) {
            System.out.println("第一层循环");
            while(true) {
                System.out.println("第二层循环");
                break;
            }
        }
    }
}

while

while,只要条件不满足停止循环。

while (boolean) {
    statement;
}

do-while

do-while 和 while/for 区别在于 do-while 中循环体不管条件满不满足都先执行 statment 一次,在进行第二次循环时才会检查条件。实际中用的少。

do {
    statement;
} while (boolean);

作用域

code block 内可以使用外面的变量,反之则不行,block 内容按照顺序执行完毕会被回收。

char str = 'a';
{
    boolean code_block_status = true;
    System.out.println(str); // 可以使用代码块外面变量。
}
code_block_status = false; // 无法使用代码块里面变量,提示找到不到此变量。

多个代码块嵌套呢?同样遵守上面规则。

char str = 'a';
{
    // 代码块1
    boolean code_block_status = true;
    System.out.println(str); // 可以使用代码块外面变量 str。
    {
        // 代码块2
        str = 'b'; // 一样可以使用
        System.out.println(str); // 可以使用代码块外面变量。
    }
}
System.out.println(str); // 证明已经被更改

数组

数组像是一个专门用于存放某一种类型数据盒子,里面分隔了很多小空间,每个空间能够存放一个数据。在 Python 中则是列表,与 Java 数组不同的是列表可以存放任意数据类型的数据并且可以创建一个空列表。

一个数组创建之后数量和类型是明确指定的,能够存储多少数据要看数据类型,存储内容时不能放入其他类型数据,创建后也不能改变其数量。

DataType[] VariableName = new ArrayType[Lenth];

访问时需要带上索引,第一个数据是从 0 开始。

VariableName[0]

下面代码 new int[10] 使用 new 关键字创建一个 int 类型数组将它长度设定为 10 ,表示能够存放 10 个 int 类型数值。接着计算机会在内存创建一段连续的地址,把这段地址第一个地址作为数组起始地址。而 int[] arrayTest 变量地址存储的值就是数组起始地址(没错,变量这个地址的值指向数组地址),由于当前数组第一个值是自己索引为 0,通过偏移量加 1 就对应获得第 2 个值,以此类推。

int[] arrayTest = new int[10];

数组在内存引用.png

数组被创建后必须初始化,像 arrayTest 就将初始化为能够存储 10 位数字的数组,如果不给它赋值将会被定义为这个类型的默认值,像 int 是 0,boolean 是 false,double 是 0.0。

System.out.println(arrayTest[0]); // 因为没有赋值所以默认值为 0

下面直接初始化的例子,一种简写方式。

// 简写方式一,不指定长度
int[] arrayTest = {
    1,
    2,
    3
};

testArray = null;
// 简写方式2
testArray = new int[] {1, 2, 3, 4}  //相当一个匿名数组赋值给变量 testArray

关于数组还有 length 属性,用于获取数组长度。这样在以后去遍历数组内容时边界条件容易确定。

arryTest.lenth; // 得到数组长度

最开始提到数组不可增加数量,它只能重新创建。

// 因为不能扩展数组,通过 new 一个新数组,原来变量指向新数组,原来数组就消失了。
String[] chars = new String[3]; // 创建 String 数组,长度为 3。
String[] chars = new String[10]; // 创建 String 数组,长度为 10。变量 chars 原来的数组指向改变。

多维数组

多维数组只是多个一维数组的嵌套,像洋葱一样剥开进行使用。下面就声明了一个二维数组,第一维声明了能够存两个值(见第二行代码),这两个值其实又是一个数组能够存 10 个值(见第 3-4 行代码)。在理解上像是俄罗斯套娃,数组套数组。

double[][] course = new double[2][10];
System.out.println(course.length);
System.out.println(course[0].length);
System.out.println(course[1].length);

二维数组在内存引用.png

使用数组

在数组使用上,通常是用循环得到数组的值。for each 用于获取数组内容,无法获取当前数组索引——很像 Python 中的 for,而 for 则都能获取,视情况用:

  1. for
  2. for each

先定义两个数组:

int[] arrayTest = {
    1,
    2,
    3
};

String[][] arrayTest2 = {
    {"字符1", "字符1.1", "字符1.2"},
    {"字符2"},
    {"字符3"}
};

for 得到其索引和值:

for (int i = 0; i < arryTest.length; i++) {
    System.out.println("arryTest 索引 " + i + " 的值是 " + arryTest[i]);
}

for (int i = 0; i < arryTest2.length; i++) {
    for (int j = 0; j < arryTest2[i].length; j++) {
        System.out.println("arryTest2 索引 " + i + " 的值是 " + arryTest2[i][j]);
    }
}

for-each:

for (int e : arrayTest) {
    System.out.println(e);
}

for (String[] e : arrayTest2) {
    for (int i = 0; i < e.length; i++) {
        System.out.println("字符串数组 e 索引 " + i + " 的值是 " + e[i]);
    }
//    上面 for 循环用到索引啦,如果不需要也可再次使用 for each 得到所有值
//    for (String x : e) {
//        System.out.println(x);
//    }
}

第一个 foreach 相当于说把 arrayTest 里的值放入 int e 变量 中,直接输出。第二个多维数组循环也是一样先把 arrayTest2 数组的内容放入 String[] e String 类型数组变量 e 中,因为它 arrayTest2 中全是数组所以变量类型也需要填写正确,里面的 for 就对 e 进行循环获取值。

参考资料

最近更新:

发布时间:

讨论讨论