Java
路线
- 阶段入门
- 编程基础(java 特点、环境搭建、基础语法、数组、面向对象、抽象类、接口、枚举、常用类、集合类、泛型、异常处理、多线程、io 流、反射)【韩顺平 30 天零基础】
- java8(Stream API、lambda 表达式、新日期时间、接口默认方法)【宋红康 java 零基础,java8 部分】
- 计算机基础
- 计算机网络(http&https 协议、网络类型、udp/tcp 协议、网络安全、域名解析)
- 操作系统(进程&线程通讯、调度、状态、死锁、内存管理)
- 企业开发基础
- mySQL 数据库(mysql 搭建、语句、约束、索引、事务、设计数据库表、性能优化)【老杜-mysql 基础入门】
- 开发框架(javaWeb、Spring5、SpringMvc、mybatis、mybatis plus、SpringBoot2、SpringSecurity、Maven)【尚硅谷系列】
- 开发规范
- Linux(进程、网络、软件包、服务、日志、linux 内核、常用命令、常用环境搭建、shell 脚本、vim 的使用)【韩顺平 linux】
- 企业开发进阶
- 设计模式(创建型、结构型、行为型)
- 中间件(Redis 缓存、RabbitMQ 消息队列、Nginx 网关)【尚硅谷系列】
- netty 网络编程(io 模型、channel、buffer、selector、netty 模型、websocket 编程-搭个聊天室)【尚硅谷】
- 微服务(dubbo、微服务、接口管理)
- 容器(docker、k8s)【狂神】
- ci/cd【狂神】
- java 高级
- 并发编程
- JVM【宋红康】
- 架构设计
- 分布式
- 高可用
- 高并发
基础语法
- 数据类型
- 流程控制
面向对象
- 方法
- 重载
- 封装
- 继承
- 多态
Spring5
- IOC
- AOP
- 事务
SpringMVC
- MVC
- 请求与响应
- restful API
- 拦截器
- 配置
- 执行过程
mybatis
- 增删改查
- 全局配置
- 动态 SQL
- 缓存
- 逆向工程
SpringBoot2
- 常用注解
- 资源整合
- 高级特性
- 本地热部署
redis 缓存
- 数据类型
- 常用操作
- java 操作 redis
- 主从模型搭建
- 哨兵模型搭建
- 日志持久化
- 应用场景
rabbitMQ 消息队列
- 消息队列的作用
- 生产消费模型
- 交换机模型
- 死信队列
- 延迟队列
- 消息持久化
- java 操作
- 集群搭建
nginx 网关
- Nginx 作用
- 正向代理
- 反向代理(负载均衡)
- 常用命令
- 动静分离(网站部署)
- 集群搭建
dubbo
- 框架演进
- RPC
- zookeeper
- 服务提供者
- 服务消费者
- 项目搭建
微服务
- 概念
- Spring cloud 框架
- Spring cloud Alibaba
基础语法
入门
看一段代码
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
保存为 Hello.java
public static void main(String[] args)
是 Java 程序的固定入口方法,Java 程序总是从main
方法开始执行
如何运行?
Java 源码本质上是一个文本文件,我们需要先用javac
把Hello.java
编译成字节码文件Hello.class
,然后,用java
命令执行这个字节码文件:
- 使用 javac 把 Hello.java compile 成 Hello.class(javac 是编译器);
- 使用 java execute Hello.class ,run on JVM(java 是虚拟机);
$ javac Hello.java
如果源代码无误,上述命令不会有任何输出,而当前目录下会产生一个Hello.class
文件
$ ls
Hello.class Hello.java
给虚拟机传递的参数Hello
是我们定义的类名,虚拟机自动查找对应的 class 文件并执行。
$ java Hello
Hello, world!
Java 11新增的一个功能,它可以直接运行一个单文件源码!
$ java Hello.java
Hello, world!
需要注意的是,在实际项目中,单个不依赖第三方库的 Java 源码是非常罕见的,所以,绝大多数情况下,我们无法直接运行一个 Java 源码文件,原因是它需要依赖其他的库。
小结
- 一个 Java 源码只能定义一个
public
类型的 class,并且 class 名称和文件名要完全一致; - 使用
javac
可以将.java
源码编译成.class
字节码; - 使用
java
可以运行一个已编译的 Java 程序,参数是类名;
IDE
IDE 是集成开发环境:Integrated Development Environment 的缩写。
使用 IDE 的好处在于,可以把编写代码、组织项目、编译、运行、调试等放到一个环境中运行,能极大地提高开发效率。
IDE 提升开发效率主要靠以下几点:
- 编辑器的自动提示,可以大大提高敲代码的速度;
- 代码修改后可以自动重新编译,并直接运行;
- 可以方便地进行断点调试。
目前,流行的用于 Java 开发的 IDE 有:
- Eclipse
- IntelliJ Idea
- NetBeans
程序基本结构
/**
* 可以用来自动创建文档的注释
*/
public class Hello {
public static void main(String[] args) {
// 向屏幕输出文本:
System.out.println("Hello, world!");
/* 多行注释开始
注释内容
注释结束 */
}
} // class定义结束
这段程序包含
- public,访问修饰符,表示该
class
是公开的(不写public
,也能正确编译,但是这个类将无法从命令行执行。); - class,定义类的关键字,java 是面向对象的语言,一个程序的基本单位就是
class
; - static,修饰符,表示静态方法;
- void,返回值类型,表示没有任何返回值;
- main,方法名;
- String[] args,参数类型和参数(Java入口程序规定的方法必须是静态方法,方法名必须为
main
,括号内的参数必须是String 数组); - 执行语句,依次顺序执行,每一行语句必须以分号结束;
- 注释,有单行注释、多行注释、文档注释;
变量
编程语言中的变量,可以理解为一种容器。
在 Java 中,变量有 3 种类型:
- 局部变量(方法体内声明)
- 实例变量(没有 STATIC 关键字声明+方法体外+特定于对象的)
- 静态变量(初始化一次+实例变量之前初始化)
class var1 {
static int a = 1; //静态变量
int data = 99; //实例变量
void method() {
int b = 90;//局部变量
}
}
变量被使用之前,需要声明、初始化;
int a; // 声明
a = 1; // 初始化
int a = 1; // 可以结合
在 java 中,在声明变量的时候,可以给它一个初始值(不给相当于给它指定了默认值。默认值总是0
)
数据类型
变量有很多类型,主要为两大类:
- 原始数据类型(布尔类型和 numeric 类型,numeric 类型包括字符型、整型、浮点型)
- 非原始数据类型(类、数组、接口等)
原始数据类型是在 Java 语言中预定义和可用的(CPU 可以直接进行运算)。原始值不与其他原始值共享状态;
有 8 种基本类型:byte、short、int、long、char、float、double 和 boolean;
原始数据类型
整数类型
byte (1 byte)
short (2 bytes)
int (4 bytes)
long (8 bytes)
计算机内存的最小存储单元是字节(byte),一个字节就是一个8 位二进制数,即 8 个bit。
一个字节表示的范围:
00000000
~11111111
(二进制)- 0~255(十进制)
00
~ff
(十六进制)
一个字节是 1byte,1024 字节是 1K,1024K 是 1M,1024M 是 1G,1024G 是 1T。
不同的数据类型占用的字节数不一样:
- byte 1
- short 2
- int 4
- long 8
- float 4
- double 8
- char 2
浮点类型
float (4 bytes)
double (8 bytes)
布尔类型
布尔类型boolean
只有true
和false
两个值
Java 语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit(0 和 1),但是通常 JVM 内部会把boolean
表示为4 字节整数。
boolean b1 = true;
字符类型
字符类型char
表示一个字符。Java 的char
类型除了可表示标准的ASCII外,还可以表示一个Unicode 字符:
char a = 'A';
char zh = '中';
注意char
类型使用单引号'
,且仅有一个字符,要和双引号"
的字符串类型区分开。
数据类型 | 默认值 | 默认字节数 |
---|---|---|
byte | 0 | 1 byte |
short | 0 | 2 bytes |
int | 0 | 4 bytes |
long | 0L | 8 bytes |
float | 0.0f | 4 bytes |
double | 0.0d | 8 bytes |
boolean | false | 1 bit |
char | ‘\u0000’ | 2 bytes |
记住:
- 所有 numeric 类型均带符号 (+/-)
- 数据类型的大小在所有平台上保持相同(标准化)
- Java 中的 char 数据类型是 2 个字节,因为它使用 UNICODE 字符集。凭借它,Java 支持国际化。 UNICODE 是一个字符集,涵盖了世界上所有已知的文字和语言
非原始数据类型
除了上述基本类型的变量,剩下的都是非原始数据类型。例如,引用类型最常用的就是String
字符串:
String s = "hello";
引用类型的变量 类似于 C 语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置。
常量
定义变量的时候,如果加上final
修饰符,这个变量就变成了常量:
final double PI = 3.14; // PI是一个常量
PI = 300; // compile error!
常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。
根据习惯,常量名通常全部大写。
常量的作用是用有意义的变量名来避免魔术数字(Magic number),例如,不要在代码中到处写3.14
,而是定义一个常量。如果将来需要提高计算精度,我们只需要在常量的定义处修改,例如,改成3.1416
,而 不必在所有地方替换3.14
。
var 关键字
有些时候,类型的名字太长,写起来比较麻烦。例如:
StringBuilder sb = new StringBuilder();
这个时候,如果想省略变量类型,可以使用var
关键字:
var sb = new StringBuilder();
编译器会根据赋值语句自动推断出变量sb
的类型是StringBuilder
。对编译器来说,它还是:
StringBuilder sb = new StringBuilder();
仅仅是少写了变量类型
变量的作用域
而在语句 块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。
{
...
int i = 0; // 变量i从这里开始定义
...
{
...
int x = 1; // 变量x从这里开始定义
...
{
...
String s = "hello"; // 变量s从这里开始定义
...
} // 变量s作用域到此结束
...
// 注意,这是一个新的变量s,它和上面的变量同名,
// 但是因为作用域不同,它们是两个不同的变量:
String s = "hi";
...
} // 变量x和s作用域到此结束
...
} // 变量i作用域到此结束
定义变量时,要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名。
类型转换
一种类型的变量可以接收另一种类型的值:
1)较小容量的变量被分配给另一个较大容量的变量
double d;
int i = 10;
d = i;
此过程是自动的,非显式转换;
2)较大容量的变量被分配给另一个较小容量的变量
double d = 10;
int i;
i = (int) d;
在这种情况下,必须显式指定类型转换运算符;
如果没有指定类型转换运算符,编译器给出错误;
由于该规则是由编译器强制执行的,因此它使程序员意识到他即将进行的转换可能会导致一些数据丢失,并防止意外丢失;
运算
整数运算
()
!
~
++
--
*
/
%
+
-
<<
>>
>>>
&
|
+=
-=
*=
/=
整数的数值是精确的,整数运算也是精确的,即使是除法也是精确的,因为两个整数相除只能得到结果的整数部分:
int x = 12345 / 67; // 184
溢出
整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,却会得到一个奇怪的结果:
public class Main {
public static void main(String[] args) {
int x = 2147483640;
int y = 15;
int sum = x + y;
System.out.println(sum); // -2147483641,(int: -2147483648 ~ 2147483647)
}
}
要解释上述结果,我们把整数2147483640
和15
换成二进制做加法:
0111 1111 1111 1111 1111 1111 1111 1000
+ 0000 0000 0000 0000 0000 0000 0000 1111
-----------------------------------------
1000 0000 0000 0000 0000 0000 0000 0111
由于最高位计算结果为1
,因此,加法结果变成了一个负数。
要解决上面的问题,可以把int
换成long
类型,由于long
可表示的整型范围更大,所以结果就不会溢出:
long x = 2147483640;
long y = 15;
long sum = x + y;
System.out.println(sum); // 2147483655
移位运算
>>
、<<
>>>
、<<<
在计算机中,整数总是以二进制的形式表示。例如,int
类型的整数7
使用 4 字节表示的二进制如下:
00000000 0000000 0000000 00000111
可以对整数进行移位运算。对整数7
左移 1 位将得到整数14
,左移两位将得到整数28
:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n << 1; // 00000000 00000000 00000000 00001110 = 14
int b = n << 2; // 00000000 00000000 00000000 00011100 = 28
int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192
int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912
左移 29 位时,由于最高位变成1
,因此结果变成了负数。
类似的,对整数 7 进行右移,结果如下:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n >> 1; // 00000000 00000000 00000000 00000011 = 3
int b = n >> 2; // 00000000 00000000 00000000 00000001 = 1
int c = n >> 3; // 00000000 00000000 00000000 00000000 = 0
如果对一个负数进行右移,最高位的1
不动,结果仍然是一个负数:
int n = -536870912; // 11100000 00000000 00000000 00000000
int a = n >> 1; // 11110000 00000000 00000000 00000000 = -268435456
int b = n >> 2; // 11111000 00000000 00000000 00000000 = -134217728
int c = n >> 28; // 11111111 11111111 11111111 11111110 = -2
int d = n >> 29; // 11111111 11111111 11111111 11111111 = -1
还有一种无符号的右移运算,使用>>>
,它的特点是不管符号位,右移后高位总是补0
,因此,对一个负数进行>>>
右移,它会变成正数,原因是最高位的1
变成了0
:
int n = -536870912;// 11100000 00000000 00000000 00000000
int a = n >>> 1; // 01110000 00000000 00000000 00000000 = 1879048192
int b = n >>> 2; // 00111000 00000000 00000000 00000000 = 939524096
int c = n >>> 29; // 00000000 00000000 00000000 00000111 = 7
int d = n >>> 31; // 00000000 00000000 00000000 00000001 = 1
对byte
和short
类型进行移位时,会首先转换为int
再进行位移。
仔细观察可发现,左移实际上就是不断地 ×2,右移实际上就是不断地 ÷2。
位运算
位运算是按位进行与、或、非和异或的运算。
与运算的规则是,必须两个数同时为1
,结果才为1
:
n = 0 & 0; // 0
n = 0 & 1; // 0
n = 1 & 0; // 0
n = 1 & 1; // 1
或运算的规则是,只要任意一个为1
,结果就为1
:
n = 0 | 0; // 0
n = 0 | 1; // 1
n = 1 | 0; // 1
n = 1 | 1; // 1
非运算的规则是,0
和1
互换:
n = ~0; // 1
n = ~1; // 0
异或运算的规则是,如果两个数不同,结果为1
,否则为0
:
n = 0 ^ 0; // 0
n = 0 ^ 1; // 1
n = 1 ^ 0; // 1
n = 1 ^ 1; // 0
对两个整数进行位运算,实际上就是按位对齐,然后依次对每一位进行运算。例如:
public class Main {
public static void main(String[] args) {
int i = 167776589; // 00001010 00000000 00010001 01001101
int n = 167776512; // 00001010 00000000 00010001 00000000
System.out.println(i & n); // 167776512
}
}
?例子
上述按位与运算实际上可以看作两个整数表示的 IP 地址10.0.17.77
和10.0.17.0
,通过与运算,可以快速判断一个 IP 是否在给定的网段内。(待补充例子)