因为都是我自己记的,所以比较零散和不规范。
算上代码什么的应该有八万多字了。
文章目录
java设计环境
一些关键术语解释
java可处理数据量庞大,而且不卡顿。
安卓就是java开发。
分布式:多台计算机合作计算同一个任务。
健壮性:代码改起来不容易出错。
体系结构中立:指的不同结构的cpu。
jdk:翻译工具。
SE 和 语言生态
SE:java核心基础
EE: 互联网开发
ME:安卓开发
下载jdk
下载好jdk,打开jdk下的bin,复制路径。
右击我的电脑,点击属性,找到高级系统设置,点击环境变量。
双击path,新建把路径粘贴进去即可。
打开cmd输入javac -version测试是否配置成功。
命令行
先用记事本写一个最简单的java程序。
另存为java文件,window编码为ANSI。
IE{BSK[R7ZHIF3LOGIW4L0.png>)
打开cmd,进入你保存文件的目录下。比如你放在了d盘,直接**d:**进入D盘,然后cd 你的文件路径即可。
javac Test.java
java Test
即可出现结果。
java基本程序结构
命名
小驼峰:第一个单词小写,后面的单词大写。
大驼峰:每个单词都大写。
变量,方法:小驼峰。
java文件名(类名):大驼峰。
注释
// 单行注释
/* */ 多行注释
数据类型 和 大小
整数:byte-8bit、short-16bit、int-32bit、long-64bit
浮点:float-32bit、double-64bit
字符:char 根据编码决定,可以是:8 16 32 bit
布尔:boolean-32bit
1B(字节) = 8bit
1 kB = 1024B 1 MB = 1024KB 1GB = 1024MB 1TB = 1024GB
范围 - 2 ^ (bit - 1) ~ 2 ^(bit - 1) - 1
整数类型
0b 二进制
0x 16进制
public class Test{
public static void main(String[] xxx){
byte x1 = 100;
short x2 = 44;
int x3 = 123;
long x4 = 1234L;
int w1 = 0b10101;
byte w2 = 0b110101;
short w3 = 0b1111101;
long w4 = 0b11101010101L;
int k1 = 076;
short k2 = 066;
int m1 = 0x12af;
long m2 = 0xab5fL;
System.out.println(w1);
System.out.println(w2);
System.out.println(w3);
System.out.println(w4);
System.out.println(k1);
System.out.println(k2);
System.out.println(m1);
System.out.println(m2);
}
}
正数使用原码来表示。
负数使用补码来表示。
浮点
float 和 double 如何表示出来的
浮点的精度丢失
public class Test{
public static void main(String[] xxx){
byte x1 = 100;
short x2 = 44;
int x3 = 123;
long x4 = 1234L;
float x5 = 45.54f;
double x6 = 234.355;
float x7 = 32534645532463456f;
System.out.println(x5);
System.out.println(x6);
System.out.println(x7);
}
}
char
32bit 位置 24bit 颜色亮度
改进:编码的由来。
编码
public class Test{
public static void main(String[] xxx){
char x1 = 'd';
char x2 = '和';
char x3 = '#';
if (x2 > x3){
System.out.println("=========");
}
int k1 = x1;
int k2 = x2;
int k3 = x3;
System.out.println(k1);
System.out.println(k2);
System.out.println(k3);
}
}
boolean
理论上1bit即可,但是电脑至少要申请1B。
布尔类型设置的大,加快运行速度,降低阻塞。
变量
变量初始化:变量首次赋值。
c语言中不赋值默认有值,而在java中没有值。
所以java中,java中声明变量一定要赋值。
常量
顾名思义,固定不变的量。
java 中使用 final 。
常量命名全部字母大写。
final 修饰基本类型+不可变类型
如数组,final可以防止整体修改,而其内部是可以修改的。
final 的作用
防止一定的指令重排序,保证多线程的可见性。
运算符
Math数学函数与常量
import java.lang.Math.*;
public class Test{
public static void main(String[] xxx){
double x1 = Math.sqrt(7);
double x2 = Math.pow(3, 4);
double x3 = Math.sin(30);
System.out.println(x1);
System.out.println(x2);
System.out.println(x3);
}
}
类型转换
虚线表示进度丢失。
强制类型转换
默认小的不能转换为大的。
可以进行强制类型转换,但是会丢失信息。
基本类型的强制转换,实际上基本没什么用。
自增和自减运算符
a++ 先运算后+1
++a 先+1后运算
import java.lang.Math.*;
public class Test{
public static void main(String[] xxx){
int a = 10;
if (++a > 10){
System.out.println("====" + a);
}else{
System.out.println("####" + a);
}
}
}
注意:b = b++; b是不会变化的。
关系运算符
== 相等
!= 不等
&& 并且
|| 或
< 小于 > 大于
三元运算符
import java.lang.Math.*;
public class Test{
public static void main(String[] xxx){
int a= 10;
int b = 10;
int m1 = a > b ? 20:30;
float m2 = a < b ? 23.5F : 34.45F;
System.out.println(m1);
System.out.println(m2);
}
}
位运算
public class Test{
public static void main(String[] xxx){
int a = 13;
int b = 59;
int m1 = a & b;
int m2 = a | b;
int m3 = a ^ b;
int m4 = ~a;
System.out.println(m1);
System.out.println(m2);
System.out.println(m3);
System.out.println(m4);
}
}
& 与
| 或
^ 异或
非代码书写:2^4 = 16
代码书写: 2^4 异或
~ 按位取反
每一位都取反。
<< >> 左移 右移
int k1 = 3;
k1 = k1 << 4; // 乘2的四次方
System.out.println(k1);
k1 = k1 >> 3; // 除以2的3次方
System.out.println(k1);
向右移动,左边补的是符号位
向左移动,右边补的是0。
> 始终补0
运算符优先级
String
字符串拼接
除了基本类型都叫引用类型。
public class Test{
public static void main(String[] xxx){
String x1 = "abcdef";
String x2 = "12314";
System.out.println(x1 + x2);
}
}
引用类型同样类型的大小就不一样了。
任何引用类型都由基本类型组成。
字符串截取
public class Test{
public static void main(String[] xxx){
String x1 = "abcdef";
String x2 = "012314";
String x3 = x2.substring(2, 4);
System.out.println(x3);
}
}
substring(2,5); // [2, 4)
与数字相接
public class Test{
public static void main(String[] xxx){
String x1 = "abcdef";
String x2 = "012314";
String x3 = x2.substring(2, 4);
System.out.println(x3);
String x6 = "afd" + 33;
int a = 90;
float b= 234.34f;
String x7 = x6 + a + b;
System.out.println(x7);
}
}
不可变字符串
由于长度会发生改变,不能在原地修改,所以引用指向别的地方。
检测字符串是否相等
经常用C++的可能会使用 ==
而在java中 == 只能判断位置是否相等。
在java中判断字符串相等使用equal。
equals 给字符串比较的时候是值,给其他引用类型比较时,仍然是地址。
字符串常量池
字符串创建时,先从常量池中寻找,要是有的话就不再创建了,而是指向它。
但如果 是 new 出来的,则它是与众不同的,不进入常量池中,而是单独创建。
空串和null串
空串和null串并不相同:
空串""是有空间的,而null是空引用不占空间。
码点与代码单元
public class Test{
public static void main(String[] xxx){
String x1 = "abcdefghijklmn";
int w1 = x1.length(); // 获取字符串的长度
char w2 = x1.charAt(3); // 获取第四个字符
int w3 = x1.codePointAt(3); // 获取第四个字符编码
int w4 = w2; // 这种获取编码的方式比较好记
System.out.println(w1);
System.out.println(w2);
System.out.println(w3);
System.out.println(w4);
}
}
String API
api:相当于一个帮助文档,可以查询现成的方法如何使用。
一些常用的:
通过看api就能知道对应如何使用:
public class Test{
public static void main(String[] xxx){
String s1= "abcdefg";
String s2 = "def";
String s3 = s1.replace("cd","kk"); //替换
int idx = s1.indexOf(s2); //子串起始位置
System.out.println(idx);
System.out.println(s3);
}
}
字符串数组
string.split(t) 根据t切割,返回字符串数组。
构建字符串
有些时候,需要由较短的字符串构建字符串,例如,按键或来自文件中的单词。采用字符串连接的方式达到此目的效率比较低。每次连接字符串,都会构建一个新的 String 对象 ,既耗时 , 又浪费空间。
使用 StringBuilder 类就可以避免这个问题的发生。
在需要构建字符串时就凋用 toString 方法,将可以得到一个 String 对象,其中包含了构建器中的字符序列。
StringBuilder();
toString();
public class Test{
public static void main(String[] xxx){
String a = "adsf";
a = a + "adfsdg";
StringBuilder b = new StringBuilder();
b.append("dfdf");
b.append("sdvfsv");
System.out.println(a);
System.out.println(b.toString());
}
}
简单对比一下时间:
public class Test{
public static void main(String[] xxx){
long start = System.currentTimeMillis();
String a ="";
for (int i = 0; i < 10000; i++){
a +="a";
}
long end = System.currentTimeMillis();
long start1 = System.currentTimeMillis();
StringBuilder b = new StringBuilder();
for (int i = 0; i < 10000; i++){
b.append("a");
}
long end1 = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(end1 - start1);
}
}
明显快很多很多。
时间戳currentTimeMillis()
所有计算机都是从1970年01月01日08时00分00秒开始计算的。
buffer机制
分页:是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。即划分逻辑地址空间至固定大小的页(Page),划分物理内存空间至固定大小的帧(Frame),并建立方案,转换逻辑地址为物理地址(pages to frames)。在 Linux 下,每一页的大小通常为 4KB。
StringBuilder 比 String 快的原因:
多个单个变量存储在多个页中,数组中的多个元素可以存在一个页中,一个页是4KB。 // 这里写的不一定对。有知道的可以纠正一下。
所以实际上的内存占用要比想象中多的多。
String字符串不可变,每次改变都要占用4kb大小去储存新的,这样内存会浪费很多,内存压力大,运行速度自然也会很慢。
而StringBuilder内存可以指定。会提前申请8kb的足够空间,每次修改就可以有充足的空间在原地址修改,自然就快了。
底层就是开辟一个较大的数组。
StringBuffer 底层就是char 数组。
像这种提前申请空间节省内存的机制叫做 buffer 机制。
常用来处理文件流,网络流。
流:本质就是基本类型数组。
String、StringBuilder、StringBuffer 区别
StringBuffer 里面加了锁,保障多线程下的安全,速度稍微慢一丢丢。也差不多,都是远远快于String。
比特流、网络流、文件流
上面也说了流本质就是基本类型数组。
如:byte二进制每8位存进数组一个数、
short二进制每16位存入数组一个数。其他与之类似。
一般都用byte数组是一个万能数组,因为其他类型都是他的倍数。
例如8位还原成32位:
文件流:文件以二进制存储,通过上述方式解析成数组后,通过编码再次解析,显示在我们的屏幕上。 不同文件的头部会有不同固定的bit来记录是哪种编码。
网络流:与文件流相同的原理。
输入输出
args 输入
利用main函数中的参数args,当然也可以起别的名字。其他语言也是一样的。输入时空格分隔。
args的作用:在程序启动时可以用来指定外部参数
Scanner 输入
需要import java.util.*导入
import java.util.*;
public class Test{
public static void main(String[] xxx){
Scanner x = new Scanner (System.in);
int x1 = x.nextInt(); // 读入整数
int x2 = x.nextInt();
System.out.println("两数之和" + (x1 + x2));
}
}
两个输入中间空格和回车隔开都可以。
同理还有:
格式化输出
每一个以 % 字符开始的格式说明符都用相应的参数替换 。 格式说明符尾部的转换符将指示被
格式化的数值类型
: f 表示浮点数 , s 表示字符串 , d 表示十进制整数 。
如:System.out.println("%.2f ", 10000.0 / 3.0) 保留两位小数
这里还有很多,不用都记住,用到的时候查就可以。
文件输入与输出
读取:
Scanner in = new Scanner(Paths.get("myfile.txt "), “UTF - 8”); // 路径,编码
文件路径含有\的话,再在前面加上一个\,否则将识别为转义字符。
写入:
PrintWriter out = new PrintWriterr(“myfile.txt”, "UTF - 8 ");
如果文件不存在,创建该文件。可以像输出到System.out—样使用print、println 以及 printf 命令。
控制流程
块作用域
块(即复合语句)是指由一对大括号{}括起来的若干条简单的 Java 语句。块确定了变量的作
用域。一个块可以嵌套在另一个块中。
不能在块的嵌套中声明相同的变量。
任何语言中其内部原理都是用栈实现的。
if 条件语句
if (条件1){
满足条件1执行;
}else if (条件2)
{
满足条件2执行;
}else{
上述都不满足执行;
}
for while 循环
for (变量; 条件; 变量的变化) // 里面也可以只有条件
{
符合条件执行;
}
while(条件){
符合条件执行;
}
还有 do while循环,先执行后判断条件。
可以写一个死循环:
import java.util.*;
public class Test{
public static void main(String[] xxx){
while(true){
System.out.println("=======");
}
}
}
Ctrl + c 可以中断执行。
switch 多重选择
switch(x){ // x 为一个变量
case 条件1:
x满足条件1执行;
break;
case 条件2:
x满足条件2执行;
break;
case 条件3:
x满足条件3执行;
break;
default:
x都不满足执行;
break;
}
switch击穿:如果没有break,匹配之后会接着往下执行。
switch可选类型:char byte short int String 枚举
break continue 中断控制流程语句
break 打断循环,但只能跳出一层循环。
continue 跳过本次循环,进行下一次循环。
大数值
导入包 import java.math.*;
基本的整数和浮点数精度不能够满足需求, 那么可以使用 jaVa.math 包中的两个很有用的类: Biglnteger 和 BigDecimaL 这两个类可以处理包含任意长度数字序列的数值。
Biglnteger 类实现了任意精度的整数运算。
BigDecimal 实现了任意精度的浮点数运算。
底层是 char 类型的数组来存的。
所以在项目中很多需要精确计算的都会使用其来代替基本类型。
注意其属于的引用类型,也不能运算符重载, 所以没有像 +、* 的操作,但可以利用如下方法:
valueOf(x) // 将x普通数值转换为大数值
a.add(b) // a + b
a.multiply(b) // a * b
数组
数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下标可以访问数组中的每一个值。
例如,如果 a 是一个整型数组,a[i] 就是数组中下标为 i 的整数。
int [] a; // 声明数组
int a[]; // 也可以这样c语言的分格写,不过一般用上一种。
int [] a = new int [100]; // 初始化创数组
数组物理地址连续。
只有基本类型的一维数组,数据才会在物理地址上严格连续。
Java中的引用类型完全等于c语言中的指针。
数组无论多大,都只记录第一个的地址。
元素地址 = 首地址 + i * m; // i 为下标(第 i + 1的位置),m 为所存的数据类型大小。
在计算机中,若一块数据没有任何一个地方记录其地址,将被标记为空白无效,被销毁。
这样也是合法的,数据类型高的容纳低的。
多维数组
二维数组的储存:
可以发现数组之间都是通过记录地址来联系,真实地址并不连续。
三维数组与之同理:
字符串类型数组
String[] arr2 = {"d","dsfs","daadf", "dd"};
也是存放的地址。
在修改 daadf 为 eeeeeee 时,不是在原位置修改,而是新创建一个再把对应位置地址指向它,而原来的 daadf 将被销毁。
Arrays.sort() 数组排序
导入import java.util.*;
Array.sort(arr) 对 arr 排序,内部和C++一样也是快排。不局限于基本类型,引用类型也可以,后面再介绍。
Array.toString(arr) 打印数组
import java.util.*;
public class Test{
public static void main(String[] xxx){
int[] arr = {1, 34, 3, 4, 5,8, 9,-1,12,33, 90};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
for each 循环
Java 有一种功能很强的循环结构,可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心 。
for ( variable : collection ) statement
import java.util.*;
public class Test{
public static void main(String[] xxx){
int[] arr = {1, 34, 3, 4, 5,8, 9,-1,12,33, 90};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
for (int w: arr){
System.out.println(w);
}
String[] arr2 = {"aa", "bb", "cc"};
for (String w : arr2){
System.out.println(w);
}
Object[] arr3 = {new Object(), new Object(), new Object()};
for (Object w : arr3){
System.out.println(w);
}
}
}
对象和类
一个文件中,可以有多个类,但是与文件名相同的public类只能有一个。
内存(面试点)
jvm内存划分:方法区、栈、堆、计数器、本地方法栈。
方法区存放类的信息和静态信息常量池、
栈存放方法执行的顺序、
堆存放对象、字符串常量池、
计数器是配合栈的、
栈中的方法翻译成操作系统所认识的方法存入本地方法栈中。
class文件用多少,进入内存多少,用不到的不进入,被关联调用,也会直接进入内存。
java中,当方法被调用时,是拷贝一份入栈,当出栈时,说明方法已经执行完毕了。
线程栈的栈顶,始终表示正在执行的方法。
java运行也占用内存。
每一个对象头,都会存储属于哪个类。
什么样的操作会堆计算机内存产生巨大的压力
io 操作会产生大量的内存消耗
概论
面向对象三大特性:封装、继承、多态、(抽象)。
类相当于图纸,根据类造出的实体叫做对象。
不加 static 的方法不能被直接通过类名调用。
加 static 的方法只有一份,而不加static的方法在每一个对象中都有。
import java.util.*;
public class Test{
public static void main(String[] xxx){
Computer x1 = new Computer();
Computer x2 = new Computer();
method1();
x1.method2();
x2.method2();
}
public static void method1(){
Computer x1 = new Computer();
method2();
x1.method2();
}
public static void method2(){
Computer.method1();
}
}
public class Computer{
public int age;
public float price;
public String type;
public String time;
public static void method1(){
}
public static void method2(){
}
}
使用预定义类
构造方法
构造方法默认会有一个,多个构造方法入参必须不一致,用于对象的初始化,名字必须与类名相同。
父类的构造方法一定优先于子类调用。
调用构造方法成功,就会在内存中形成一个对象。
当我们输出对象时,默认输出的是其类型与地址,现在一般为8位。
当然每个类中都默认有一个toString方法是返回的当前类型和地址,是从父类继承过来的,所以我们重写一下,就可以输出我们想要的了。
输出时默认调用 toString 方法,我们可以加上,效果是一样的。
import java.util.*;
public class Test{
public static void main(String[] xxx){
Computer x1 = new Computer(34);
Computer x2 = new Computer("24", 99);
Computer x3 = new Computer();
System.out.println(x1);
System.out.println(x2);
System.out.println(x3);
System.out.println(new Computer(34).toString());
System.out.println(new Date().toString());
}
}
public class Computer{
public int age;
public float price;
public String type;
public String time;
public String toString(){
return "123====";
}
public Computer(){
System.out.println("========");
}
public Computer(int x){
System.out.println("@@@@@@");
}
public Computer(String y, int m){
System.out.println("&&&&&&&");
}
public static void method1(){
}
public static void method2(){
}
}
注意:像非静态的方法(不带static方法)依托于对象,若只是声明变量,不new的话,根据上面内存的讲解中,其对象和方法不会创建出来,自然就不能被调用了。
如:Date d;
d.toString(); // 错误的
引用类型等号两边是表示同一个指向,这与c++中的指针类似。
localDate 类
localDate.now(); 会构造一个新对象,表示构造这个对象时的日期。
通过类名调用,说明是静态方法。 一个静态方法为什么能创建对象呢?不是因为其是静态的,而是因为此方法返回了一个new的对象。 如:
注意:java中方法是拷贝一份再去执行的,所以此方法多次使用创建的是不同的对象,而非同一个。
localDate.of; // 显然也是个静态方法。
说明此三个方法就是非静态方法。
值传递 和 引用传递
java中实际上其实只有值传递。
所以像下面的代码,只有m2方法是可以改变a.age的值的,而m1不行,他只是改变了p1,p2的指向。
import java.util.*;
public class Test{
public static void main(String[] xxx){
Person a = new Person();
a.age = 33;
Person c = new Person();
c.age = 10;
m1(a, c);
System.out.println(a.age);
m2(a, c);
System.out.println(a.age);
}
public static void m1(Person p1, Person p2){
Person p3 = p1;
p1 = p2;
p2 = p3;
}
public static void m2(Person p1, Person p2){
int w = p1.age;
p1.age = p2.age;
p2.age = w;
}
}
public class Person{
public int age;
public String name;
}
另一个例子来更好的理解引用传递。如下:
更改器 与 访问器
访问器:类中的一个公共方法,返回本类中的私有属性,传输给外界,就叫访问器。
更改器:类中的一个公共方法,对于本类中的私有属性进行修改,就叫更改器。
public class Person{
public int age;
public String name;
public Person child;
public int m1(){ // 访问器
return age;
}
public void m2(int x){ // 更改器
age = x;
}
}
访问器其实在不做任何处理的情况下是并不安全的。就比如返回了一个私有数组,返回时,会接收到它的地址,从而就可以对其中元素进行修改,所以并不安全,需要添加额外的代码来使其安全。
下面给出例子:
import java.util.*;
public class Test{
public static void main(String[] xxx){
Person a = new Person();
int w = a.m1();
int[] ss = a.m3();
ss[0] = 99;
int[] ww = a.m3();
System.out.println(ww[0]);
}
}
public class Person{
private int age;
public String name;
public Person child;
private int[] arr = {1, 2, 3, 4, 5};
public int m1(){ // 访问器
return age;
}
public void m2(int x){ // 更改器
age = x;
}
public int[] m3(){
return arr;
}
}
可以发现对其成功修改。
当然我们可以对于Person类中的m3方法进行改进。
public int[] m3(){
int[] qq = new int[arr.length];
for (int i = 0; i < arr.length; i++)
qq[i] = arr[i];
return qq;
}
数组中的元素是基本类型数组,每次调用都拷贝一份,来作为返回,就算修改,也是修改的拷贝后的数组中元素,不会对原数组进行修改,这样就可以让其地址不外露,达到访问器安全的效果。
深拷贝 与 浅拷贝
所有的基本类型都是深拷贝。
两个引用变量指向同一个区域是浅拷贝。
引用类型想要进行深拷贝,需要另开辟自己的空间对其内部基本类型进行拷贝后传递。
注意:虽然String为引用类型,但其由于不可变,就算使另一个变量指向它并且对其修改也是新创建一个字符串来从新指向,原String不会改变。
实现一个日历
import java.time.*;
public class CalendarTest{
public static void main(String[] args)
{
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();
date = date.minusDays(today - 1);
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++)
System.out.print(" ");
while( date.getMonthValue() == month)
{
System.out.printf("%3d", date.getDayOfMonth());
if (date.getDayOfMonth() == today)
System.out.print("*");
else
System.out.print(" ");
date = date.plusDays(1);
if ( date.getDayOfWeek().getValue() == 1) System.out.println();
}
if (date.getDayOfWeek().getValue() != 1) System.out.println();
}
}
自定义类
Employee类 例子
一个文件中,可以有多个类,但是与文件名相同的public类只能有一个。
import java.time.*;
public class EmployeeTest
{
public static void main(String[] args)
{
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10,1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3,15);
for (Employee e: staff)
e.raiseSalary(5);
for (Employee e: staff)
{
System.out.println("name=" + e.getName() + ", salay=" + e.getSalary() + ", hireDay=" + e.getHireDay());
}
}
}
class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
内存图:
多个源文件文件使用
可以将 Employee类 和 EmployeeTest类 分别存放在不同的java文件中。
在编译时可以javac Employee*.java,让所有Employee开头的文件编译。
当然也可以只javac EmployeeTest.java,编译器会自动搜索用到的类并且编译。
构造器
构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
隐式参数 this
this 指的当前方法所属的对象。
便于将实例域与局部变量明显地区分开。
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
this.salary += raise; // 这样用
}
封装
就像遥控器只用知道每个按钮的功能,不需要明白其内部构造一样。
也就是不能暴露内部的地址,将自己隐藏起来。
所以,在访问器获取对象属性时,根据经验,基本类型我们可以直接返回,而引用类型我们需要返回其拷贝。当然String是不可变的,也可以直接返回。
final
只能用于修饰基本类型和不可变类型。
修饰可变类型时,指向不能变,但其内容可以修改。
final Person a = new Person();
a = new Person(); // 不行
a.age = 100;
静态域 与 静态方法
被 static 修饰的就是静态的。
静态域
如果将域定义为 static , 每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。 换句话说,静态域是每一个对象共享的。
静态常量
final 定义的是常量,本身就不可变,只要有一份就可以了,所以一般配合 static 使用,若不设为静态,每个对象中都会创建一份,造成内存的浪费。
public static final double PI = 3.14159265358979323846;
静态方法
静态方法不能访问实例域,因为它不能操作对象,但可以访问静态域。
静态方法可以通过类名直接调用。
main 方法
只有主方法,才能接受外界的刺激。
有主方法的才能是进程,否则是线程。
方法参数
java中其实是没有引用传递的,只有值传递。 (面试点)
实际上, 对象引用是按值传递的。
例子:
public class ParamTest
{
public static void main(String[] args)
{
// 例子 1
System.out.println("Testring tripleValue:");
double percent = 10;
System.out.println(" Before : percent " + percent);
tripleValue ( percent ) ;
System.out.println (" After : percent = " + percent) ;
// 例子 2
System.out.println(" \nTesting tripleSalary:" );
Employee harry = new Employee("Harry", 50000);
System.out.println("Before : salary = " + harry.getSalary ());
tripleSalary(harry);
System.out.println("After : salary = " + harry.getSalary());
// 例子 3 C++中可以完成指针的交换,java不行
System.out.println("\nTesting swap:");
Employee a = new Employee("Alice", 70000);
Employee b = new Employee("Bob", 60000);
System.out.println("Before : a = " + a.getName());
System.out.println("Before : b = " + b.getName());
swap(a, b);
System.out.println("After : a = " + a.getName());
System.out.println("After : b = " + b.getName());
}
public static void tripleValue(double x)
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
public static void tripleSalary(Employee x)
{
x.raiseSalary(200);
System.out.println("End of method: salary=" + x.getSalary());
}
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
class Employee
{
private String name;
private double salary;
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
this.salary += raise;
}
}
例子内存原理解释:
例 1:
例 2:
例 3:
对象构造
重载
如果多个方法,比如有相同的名字、不同的参数,便产生了重载。
注意:返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为 false、对象引用为 null 然而,只有缺少程序设计经验的人才会这样做。确实,如果不明确地对域进行初始化,就会影响程序代码的可读性 。
简单来说,就是我们 new 一个对象,如果不给其对象的属性赋值,那么其就会有默认值。
注意:我们在方法中定义的变量是没有默认值的,只有对象里才有,注意区分。
无参构造
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为 0 、布尔型数据设置为 false 、所有对象变量将设置为 null 。
注意:如果类中提供了至少一个构造器, 但是没有提供无参数的构造器,则在构造对象时如果
没有提供参数就会被视为不合法。
显式域初始化
也就是给类的属性赋一个初值而已。
4.6.6 调用另一个构造器
构造器之间可以相互调用。
要放在第一行。
4.6.7 初始化块(面试大重点)
由 {} 构成。
执行优先级:静态代码块 > 代码块 > 构造方法
静态代码块和静态属性优先级相同,谁在上面谁先执行。
import java.util.*;
public class ConstructorTest{
public static void main(String[] args){
Employee[] staff = new Employee[3];
staff[0] = new Employee("harrry",4000);
staff[1] = new Employee(60000);
staff[2] = new Employee();
for (Employee e: staff){
System.out.println("name="+e.getName() + ", id=" + e.getId() + ",salary=" + e.getSalary());
}
}
}
class Employee
{
private static int nextId;
private int id;
private String name="";
private double salary;
static{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
{
id = nextId;
nextId++;
}
public Employee(String n, double s)
{
name = n;
salary = s;
}
public Employee(double s)
{
this("employee #" + nextId, s);
}
public String getName()
{
return name;
}
public Employee(){
}
public double getSalary()
{
return salary;
}
public int getId(){
return id;
}
}
public class Person{
public int age;
public String name;
public Person(){
System.out.println("我是构造方法");
}
{
System.out.println("我是代码块");
}
static{
System.out.println("我是静态代码块");
}
}
public class Test{
public static void main(String[] args){
Person a = new Person();
}
}
但静态代码块只执行一次,在第一次类加载进入内存,以后不再执行。
public class Test{
public static void main(String[] args){
Person a = new Person();
Person b = new Person();
Person c = new Person();
}
}
特别的,静态的只能由首个正在创建的对象生成。例如(注意执行顺序):
public class Test{
public static void main(String[] args){
Person a = new Person();
Person b = new Person();
}
}
public class Person{
public int age;
public String name;
public Person(){
System.out.println("我是构造方法"); // 3 7 9
}
{
System.out.println("我是代码块1"); // 2 6 8
}
static{
System.out.println("我是静态代码块1"); // 1
}
public static Person w = new Person(); // 先生成,但不是首个正在创建,不能执行下面代码的静态代码块。
static{
System.out.println("我是静态代码块2"); // 4
}
static{
System.out.println("我是静态代码块3"); // 5
}
}
继承情况下的执行顺序:(面试题)
父类静态静态—子类静态—父类非静态—子类非静态
继承
extends 继承
超类 与 子类
java中一个类只能有一个父类,但是可以有多个孩子类。
final
修饰的类不能被继承,修饰的方法不能被重写。
多态
指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
在继承状态下,祖宗类可以等于子孙类,形成多态。
new一个子类对象时,一定会先生成父类对象。
public class A{
public int age;
public String name;
public void m1(){
System.out.println("Am1");
}
}
public class B extends A{
public int height;
public void m2(){
System.out.println("bm2");
}
public void m1(){
System.out.println("bm1");
}
}
public class C extends A{
public void m1(){
System.out.println("cm1");
}
}
public class Test{
public static void main(String[] ss){
A x1 = new A();
x1.m1();
x1 = new B();
x1.m1();
x1 = new C();
x1.m1();
}
}
super 调用父类构造方法
要放在子类构造方法的第一行,不然会报错。
父类的private属性不会被继承,但是在new子类时,父类会先创建出来,可以用过super的public方法来构造出来。
如父类构造方法有参,那么子类super必须也要有参数。
强制类型转换
若 B 继承 A,C 继承 A。
public class Test{
public static void main(String[] ss){
A x1 = new A();
x1.m1();
x1 = new B();
x1.m1();
x1 = new C();
x1.m1();
B x2 = new B();
C x3 = new C();
x2 = (B)x3; // 错误语法,没有继承关系
x1 = x2; // 合法,显然子类是父类的扩充,所以由多变少是合情合理的
x2 = x1; // 违法
x2 = (B)x1; // 合法,因为有继承关系又做了强则类型转换,但不一定对。只有x1就指向B或其子孙才能转换成功。依然要保持多态特性。
}
}
简单来说就是类型向下需要强转,但是指向仍然要符合向上的多态才能强转成功。
报错:cast 表示为转化,项目中经常会遇到。
Object 只能被泛型强转。
instanceof
对象A instanceod 类型B
判断 对象A 是不是 B 类型或则其子类的对象。
在强则类型转换前要先判断一下,防止程序中断。
abstract 抽象类
抽象:意思是提取共性,具体方法不同,这就是抽象的意义。 比如: 不同的图像都可以计算周长,但是计算方法不同。
抽象方法不用写具体实现。
包含抽象方法的类必须是抽象的。
抽象方法也符合多态。
所以在子类继承一个抽象类时,若不重写其中的抽象方法,那么子类也必须是抽象类。
Object 类
是所有类的父类。
Object 方法
equal() 比较两个对象是否是引用同一个对象。
getClass() 获取对象的所属类型。
hashCode() 哈希散列码
每一个生成的对象都会有的码,相当于身份证,但是会有很小的概率相同。
wait() 让线程进入等待状态
notify() 唤醒一个线程
notifyAll() 唤醒全部线程执行
equal 的作用?
Object 中 equal 判断两个对象是否指向同一地址。
String 中 equal 判断两个字符串是否相等,因为字符串重写的equal。
5.2.3 hashCode 方法(重点)
每一个生成的对象都会有的码,相当于身份证,但是会有很小很小的概率相同。
散列码不一样的对象一定不一样。
重写equal必须重写hashCode
一般来说hashCode是根据地址计算的。
字符串的hashCode就被重写了,如果内容一样,则hashCode的散列码也一样。
为什么必须重写hashCode?
hashmap 需要equal与hashCode配合,项目中基本上是必须用到的。所以重写equal必须重写hashCode。
hashCode 与 equal 如何打配合?
待补充。
对象包装器 与 自动装箱
short -> Short
long -> Long
包装类由泛型而产生。
拆箱 与 装箱
装箱: 基本类型 变 引用类型
拆箱:引用类型 变 基本类型
128 陷阱(面试点)
Integer 在 -128 ~ 127 是相等的,公用一共对象,超出就不相等了。
参数数量 可变 的方法
相当于一个数组而已。
枚举 类
限定了对象个数。
枚举类型的构造方法不能公有,不然就被new了。
public enum AAA {
AA(10, "小明"), BB(20,"大明"), CC(40, "小花"), DD(15, "小与"), WW(50, "小李");
private int age;
private String name;
// 枚举类型的构造方法不能公有,不然就被new了
private AAA(int x, String y) {
age = x;
name = y;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
反射(大重点,完整背过)
几种获取 类信息 的方式?(面试点)
三种。
// 1 通过 对象
Employee e;
Class cl = e.getClass();
// 2 通过 路径
String dassName = "java.util.Random";
Class cl = Class.forName(dassName);
// 3 通过 类
Class dl = Random.class; // if you import java.util
Gass cl 2 = int.class;
Class cl 3 = Double[],class;
newInstance()
反射之后,通过newInstance() 创建类对象
例如:
e.getClass().newInstance();
5.7.3 利用 反射 分析类
反射可以获取类的一切信息。
类名字(类修饰,类注解)
属性域(修饰,类型,名字,注解)
方法(修饰符, 返回类型,入参数量,入参类型)
获取 注解反射 包下 TestRelect 类。
package 注解反射;
import java.util.*;
import java.lang.reflect.*;
public class ReflectionTest {
public static void main(String[] args) {
String name = "注解反射.TestRelect";
try {
// 根据 路径 获取类信息对象
Class c1 = Class.forName(name);
// 获取父类
Class superc1 = c1.getSuperclass();
// 获取类 修饰
String modifiers = Modifier.toString(c1.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print("class " + name);
if (superc1 != null && superc1 != Object.class) System.out.print(" extends " + superc1.getName());
System.out.print("\n{\n");
// 打印 构造方法
printConstructors(c1);
System.out.println();
// 打印 方法
printMethods(c1);
System.out.println();
// 打印 属性
printFields(c1);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
private static void printFields(Class c1) {
// 获取 属性信息对象 数组
Field[] fields = c1.getDeclaredFields();
for (Field f: fields) {
// 获取 属性类型
Class type = f.getType();
// 获取 属性名字
String name = f.getName();
System.out.print(" ");
// 获取 属性修饰
String modifiers = Modifier.toString(f.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.println(type.getName() + " " + name + ";");
}
}
private static void printConstructors(Class c1) {
// 获取 构造信息对象数组
Constructor[] constructors = c1.getDeclaredConstructors();
for (Constructor c: constructors) {
// 获取 构造方法名字
String name = c.getName();
System.out.print(" ");
// 获取 构造方法属性修饰
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(name + "(");
// 获取 参数列表信息 类 对象数组
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) System.out.print(", ");
// 输出参数类 名字
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
private static void printMethods(Class c1) {
// 获取 方法信息对象
Method[] methods = c1.getDeclaredMethods();
for (Method m: methods) {
// 获取 方法返回信息 对象
Class retType = m.getReturnType();
// 获取 方法名字
String name = m.getName();
System.out.print(" ");
// 获取 方法修饰 字符串
String modifiers = Modifier.toString(m.getModifiers());
if (modifiers.length() > 0) System.out.print(modifiers + " ");
System.out.print(retType.getName() + " " + name + "(");
// 获取参数类信息对象数组
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
}
}
运行结果:
5.7.4 在 运行 时使用反射分析对象
learn1:
package 注解反射;
import java.lang.reflect.Method;
public class Learn1 {
public static void main(String[] args) throws Exception {
String url = "aaaawwww";//假设这是socket获取的http协议中截取到的请求servlet的注解路径
// war包都有WEB-INF目录,里面有classes文件夹,全部的java文件一定都在这个目录下
//tomcat遍历这个目录,获取到全部的文件路径,路径属于字符串类型,文件可能包含非java类型文件
//通过java的字符串自带的方法识别后缀名带有.class的文件就是java文件(str.endsWith(".class")方法进行判断)
//识别出路径是java类型的,存入数组arr
//com.操作系统.AAA com.操作系统.bbb. com.操作系统.ccc
String[] arr = {"注解反射.TestDemo2", "注解反射.TestDemo3", "注解反射.TestDemo4","注解反射.Test3"};
Class<?>[] cls = new Class[arr.length];
for(int i =0;i < cls.length; i++) {
cls[i] = Class.forName(arr[i]);
// 获取此类的 WebServlet2注释 的信息,参数是所需要的注释类信息
WebServlet2 annotation = cls[i].getAnnotation(WebServlet2.class);
if(annotation!=null) {
//模拟挑选出了servlet(带有注解的类)
String urlname = annotation.url();
if(url.equals(urlname)) {
System.out.println(arr[i]);
Object x = cls[i].newInstance();
HttpServletDemo w = (HttpServletDemo) x;
w.doGet(22, "www");
//Method method = cls[i].getDeclaredMethod("doGet", new Class[] {int.class, String.class});
//method.invoke(x, 34,"大家好");
}
}else {
continue;
}
}
}
}
WebServlet2:
package 注解反射;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD,ElementType.TYPE})
public @interface WebServlet2 {
public int age();
// 为name变量指定初始值
public String url() default "张三";
}
TestDemo2:
package 注解反射;
@WebServlet2(url = "aaaawwww",age=12)
public class TestDemo2 extends HttpServletDemo{
public void doGet(int x, String y){
System.out.println("我是TestDemo2" + ",x=" + x + ",y=" +y);
}
}
File 文件操作
File对象:就表示一个路径,文件或文件夹路径。
注意:File 可存在可不存在。
三种 构造方法
- 传入文件路径
- 传入父级路径 和 子级路径
- 父级对象与子级路径进行拼接
一般不会自己去拼接。
作用:将path转换为对象。
package com.file.test1;
import java.io.File;
public class filedemo1 {
public static void main(String[] args) {
String str = "E:\\test\\test2\\test3\\aaa.txt";
// 1. 传入文件路径
File f1 = new File(str);
System.out.println(f1); // E:\test\test2\test3\aaa.txt
// 2. 传入父级路径 和 子级路径
String parent = "E:\\test\\test2\\test3";
String child = "aaa.txt";
File f2 = new File(parent, child);
System.out.println(f2); // E:\test\test2\test3\aaa.txt
// 自己拼接,一般不用,换个系统就可能发生错误
File f3 = new File(parent + "\\" + child);
System.out.println(f3); // E:\test\test2\test3\aaa.txt
// 3. 父级对象与子级路径进行拼接
File parent2 = new File("E:\\test\\test2\\test3\\aaa.txt");
String child2 = "aaa.txt";
File f4 = new File(parent2, child2);
System.out.println(f4); // E:\test\test2\test3\aaa.txt\aaa.txt
}
}
File 遍历
IO 流
IO流:存储和读取数据的解决方案。
分类
纯文本文件:利用记事本打开可以读懂的就算纯文本。
输出和输入都是对应程序而言的,可以避免我们记混。
字节 流
FileOutputStream
- 创建对象,指定位置(产生数据传输通道)
参数可以是File对象,也可以是路径
如果文件不存在,且父级路径正确,会新建文件
如果文件存在,会清空文件 - 写出数据
ASCII对应 字符
可以传入字节流,指定起始位置,长度 - 释放资源
解除资源占用
package com.io.testdemo1;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class test1 {
public static void main(String[] args) throws IOException {
// 创建对象,指定位置(产生数据传输通道)
FileOutputStream fos = new FileOutputStream("src/aaa.txt");
// 写入数据
fos.write(97);
byte[] bytes = {97, 98, 99, 100};
fos.write(bytes);
fos.write(bytes, 0, 2);
// 释放资源
fos.close();
}
}
换行 与 续写
换行:
write(“\r\n”) 即可 linux只写\n即可 mac写\r
\r 表示回车 \n 表示换行
早期\r表示,回到此行开头,\n才表示换行,一直沿用了下来。
续写:
在输出流对象的第二个参数中,加入true,表示打开续写开关。
例子:
package com.io.testdemo1;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class test1 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("src/aaa.txt", true);
// 写入数据
String str1 = "hello";
byte[] bytes1 = str1.getBytes();
fos.write(bytes1);
// 写入换行
String str2 = "\r\n";
byte[] bytes2 = str2.getBytes();
fos.write(bytes2);
fos.close();
}
}
运行两次的结果:
可以发现第二次续写了,并没有清空,同时换行了。
FileInputstream
与输出相似。
- 创建对象(搭建桥梁)
如果文件不存在则直接报错 - 读入(返回值为int)
一次读一个字节,ASCII对应的数字 (每次读相当于一次指针的移动)
读到末尾时返回-1 - 释放资源
package com.io.testdemo2;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class test2 {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("src/aaa.txt");
// 循环 读入
int b; // 用变量去接收,要是条件和循环体内都read会跳两次
while ((b = fis.read()) != -1) {
System.out.print((char)b);
}
// 释放资源
fis.close();
}
}
运行结果与文件内容相同,说明成功读取成功:
实现 文件拷贝(另一种读入方法)
read可以传入byte数组,这样可以一次读入一个字节数组大小,速度会快很多,大小最好为1024的整数倍。
注意:返回值变成了长度,读完会返回-1。
将aaa.txt拷贝bbb.txt:
package com.io.copydemo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class test3 {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src/aaa.txt");
FileOutputStream fos = new FileOutputStream("src/bbb.txt");
byte[] bytes = new byte[4096];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
fis.close();
fos.close();
}
}
运行结果:
编码
不要用字节流取读取文本文件,会乱码。
乱码:字节截取不对或编码不对照成。
编码和解码要统一。
idea默认utf-8,字母1字节,汉字3字节
eclipse默认jbk,字母1字节,汉字2字节
package com.io;
import java.io.UnsupportedEncodingException;
public class demo4 {
public static void main(String[] args) throws UnsupportedEncodingException {
String str = "abc你好";
byte[] bytes = str.getBytes("GBK");
String res1 = new String(bytes, "GBK");
String res2 = new String(bytes, "UTF-8");
System.out.println(res1); // abc你好
System.out.println(res2); // abc���
}
}
字符流
字符流底层就是字节流。
字符流 = 字节流 + 字符集
特点:
输入流:一次读入一个字节,遇到中文时,一次读多个字节。
输出流:底层会把数据按照指定的编码精选编码, 变成汉字。
用于对应纯文本操作。
FileReader
- 创建字符流对象
- 读取数据
无参,返回int, 读到末尾返回-1
有参,传入char[],返回长度
默认也是一个一个字节读,还换转换成10进制返回
强转为char即可 - 释放资源
无参读入:
package com.io.testdemo5;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test5 {
public static void main(String[] args) throws IOException {
// 创建字符流对象
FileReader fr = new FileReader("src\\ccc.txt");
// 读取数据
int ch;
while((ch = fr.read()) != -1) {
System.out.print((char)ch);
}
// 释放资源
fr.close();
}
}
传入char[] 读入:
package com.io.testdemo5;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Test5 {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("src\\ccc.txt");
char[] chars = new char[10];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.print(new String(chars, 0, len));
}
fr.close();
}
}
其实是将转十进制,强转为char,放入数组中,这三步合并了。
FileWrite
- 创建对象,指定位置(产生数据传输通道)
参数可以是File对象,也可以是路径 - 写出数据
可以传入字符数组,指定起始位置,长度
也可以传入字符串
或者int十进制,会自动转为字符 - 释放资源
解除资源占用
package com.io.testdemo6;
import java.io.FileWriter;
import java.io.IOException;
public class Test7 {
public static void main(String[] args) throws IOException {
// true表示续写
FileWriter fw = new FileWriter("src/ccc.txt", true);
// 传入整数,字符串,字符数组都可以
fw.write(25105); // 我
fw.write('一');
fw.write("你好阿\r\n");
fw.write(new char[] {'a', 'b', 'c'});
fw.close();
}
}
缓冲流
基本流的包装流。
底层自带8192缓冲区。
由于是在内存中的操作,提高了运行速度。
字节缓存流 读入和输出
用法与基本流完全一致,不在详细介绍。
关闭缓冲流时自动关闭传入的基本流。
例子:拷贝文件
package demo1;
import java.io.*;
import java.nio.Buffer;
public class Test1 {
public static void main(String[] args) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("aaa.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bbb.txt"));
// 一个一个读入和输出
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
// 字节数组读入和输出
byte[] bytes = new byte[4096];
int len;
while ((len = bis.read()) != -1) {
bos.write(bytes);
}
bis.close();
bos.close();
}
}
运行结果:拷贝成功
字符 缓冲流
与基本字符流用法相似,不再详细介绍。
学习两个重要的方法,是基本字符流没有的,方便我们使用。
readline() // 读入一行,不会读入换行符,返回String
newLine() // 换行输出
例子:拷贝txt文件
package demo2;
import java.io.*;
import java.nio.Buffer;
public class Test2 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("aaa.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("bbb.txt"));
// 直接获取一行,不会读取换行符
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 换行
}
bw.close();
br.close();
}
}
转换流
是字符流和字节流之间的桥梁。
在jdk11后已经弃用。
新转换法的用法与基本字符流相识,多了个指定编码的参数。
例子:
将gbk编码txt文件拷贝成utf-8的txt文件
旧方法,已弃用:
package demo3;
import java.io.*;
import java.nio.charset.Charset;
public class Test3 {
public static void main(String[] args) throws IOException {
// 创建对象
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\桌面\\test\\aaa.txt"), Charset.forName("GBK"));
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\桌面\\test\\bbb.txt"), Charset.forName("UTF-8"));
// 读入和输出
int b;
while ((b = isr.read()) != -1) {
osw.write(b);
}
// 释放资源
osw.close();
isr.close();
}
}
新方法:
package demo3;
import java.io.*;
import java.nio.charset.Charset;
public class Test3 {
public static void main(String[] args) throws IOException {
// 新方法,我是1.8版本,好像不能这样用
FileReader fr = new FileReader("D:\\桌面\\test\\aaa.txt", Charset.forName("GBK"));
FileWriter fw = new FileWriter("D:\\桌面\\test\\bbb.txt", Charset.forName("UTF-8"));
int b;
while ((b = fr.read()) != -1) {
fw.write(b);
}
fw.close();
fr.close();
}
}
序列化流
是字节流的包装流。
用于读取和输出java对象。
注意:对象类需要实现Serializable接口,此接口内没有方法,用于标记。
输出后的序列化文件不能修改,否则不能反序列化。
对象的类在序列化后再修改,也不能再反序列化。也是因为序列号变化,导致不一致。
可以给类中加一个如下固定序列号:
private static final long serialVersionUID = xxxL;
如下设置,可以让没有加的类变黄自动提示。
例子:
package demo4;
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 5826262506373399078L;
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "name=" + name + "age=" + age;
}
}
package demo4;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class test4 {
public static void main(String[] args) throws IOException {
// 创建对象
Student stu = new Student("lpy", 3);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("aaa.txt"));
// 写文件
oos.writeObject(stu);
// 释放资源
oos.close();
}
}
运行结果:
反序列化流
很明显,是用于把序列化的对象,读到内存中。
package demo5;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
public class test5 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("aaa.txt"));
// 获取文件中的对象
Object o = ois.readObject();
// 打印
System.out.println(o);
// 释放资源
ois.close();
}
}
运行结果:
序列化多个对象
如果序列化多个,可以把对象加入ArrayList中,然后序列化ArrayList,它也实现了Serializable接口。
打印流
只能输出(写),不能读。
特点:
- 只能操纵目的地,不能操纵数据源。
- 特有的写出方法,可以让数据原样输出。
- 特有的写出方法,可以实现自动刷新和自动换行。
字节打印流
没有缓冲区,刷不刷新每区别。
package demo6;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
public class Test5 {
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
// 创建对象, 自动刷新,utf-8编码
PrintStream ps = new PrintStream(new FileOutputStream("aaa.txt"), true,"UTF-8");
// 输出
ps.println(123);
ps.print("abc");
ps.printf("%s", "你好"); // 这个与c类似
// 释放资源
ps.close();
}
}
运行结果:
字符打印流
System.out就是打印流,默认指向控制台,唯一,不能关闭。
package demo7;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
public class Test7 {
public static void main(String[] args) throws IOException {
// 创建对象,自动刷新
PrintWriter pw = new PrintWriter(new FileWriter("aaa.txt"), true);
// 输出
pw.println(123);
pw.print("abc");
pw.printf("%s", "你好");
// 释放资源
pw.close();
}
}
运行结果:
并发
高速缓冲的意义
核心从内存加载到高速缓存,解放了总线,核心直接和高速缓存交互。
不过也产生了一些其他问题。
例如:
package demo2;
public class TreadTest1 {
public static void main(String[] args) throws InterruptedException {
int[] a = {0};
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 50000; i++) {
a[0]++;
}
}
};
Thread t2 = new Thread() {
public void run() {
for (int i = 0; i < 50000; i++) {
a[0]++;
}
}
};
t1.start();
t2.start();
t1.join(); // 不会对t2执行造成影响,因为t2已经start
t2.join();
System.out.println(a[0]); // 2~1000000000 结果极端情况
}
}
运行结果:
这就是高速缓冲,带来的并发弊端。
高速缓冲往回更新时,只要更新成功,里面的数据就没了。
预防空转:
Thread.sleep(0); // 睡眠0毫秒,立刻让出cup,进入就绪队列。
进程
线程状态:新建、就绪、等待、阻塞、死亡。
不公平分配cpu,先进入就绪队列的不一定先执行。
每个进程被执行多少时间都是不确定的,由操作系统实现。
小细节
run()里不能改变外面的值。
但是可以修改引用类型里的值,不过指向任然不能改变。
就像加了final一样
join() 表示该进程执行完毕在执行主进程。
什么是多线程
简单理解:进程就是一个运行的软件,而线程是软件中的一个功能,多个功能就组成了多线程。
作用:提高了运行效率。
多线程 三种 实现方法
继承 Thread 的方法
- 自定义类继承 Thread 重写run方法
- new子类,然后调用 start() 方法,启动线程
- 对象setName(“线程1”) 可以设置线程名字,在类中可以用getName() 来获取
例:
package com.study;
// 自定义类继承 Thread
public class MyThread extends Thread{
// 重写run方法
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ": helloworld");
}
}
}
package com.study;
public class ThreadDemo {
public static void main(String[] args) {
// new子类
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 设置线程名字
t1.setName("线程1");
t2.setName("线程2");
// 调用 start() 方法,启动线程
t1.start();
t2.start();
}
}
运行截图:
实现 Runnable接口 的方法
- 定义一个类实现Runnable接口
- 重写run方法
- 创建自己的类对象
- 创建Thread对象,开启线程
注意:也可以setName,但是要记住,这种方法,类中不能直接使用getName方法,因为并没有继承Thread,自然类中没有此方法。但是可以在类中获取当前正在运行线程的对象,来获取name。
例子:
package com.study.threaddemo2;
// 定义一个类实现Runnable接口
public class MyRun implements Runnable{
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Thread h = Thread.currentThread(); // 获取当前线程的对象
System.out.println(h.getName() + ":helloworld");
}
}
}
package com.study.threaddemo2;
public class Test2 {
public static void main(String[] args) {
// 创建自己的类对象
MyRun r1 = new MyRun();
MyRun r2 = new MyRun();
// 创建Thread对象
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
// 设置线程名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程
t1.start();
t2.start();
}
}
运行截图:
实现 Callable接口 并利用 FutureTask类 来接收返回值 的方法
- 实现Callable接口
- 重写call方法,有返回值
- 创建自己类的对象
- 创建FutureTask对象(管理多线程运行结果)
- 创建Thread类对象且启动
注意:Callable泛型,填入返回的类型。
例子:
package com.study.threaddemo3;
import java.util.concurrent.Callable;
// 实现Callable接口
public class MyCallable implements Callable<Integer> {
// 重写call方法
public Integer call() {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
package com.study.threaddemo3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建自己类的对象
MyCallable mc = new MyCallable();
// 创建FutureTask对象
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建Thread类对象且启动
Thread t1 = new Thread(ft);
t1.start();
// 获取返回值
Integer res = ft.get();
System.out.println(res);
}
}
运行结果:
我的理解 和 总结
继承Thread类 的方法,实际就是自己创建了一个流水线,并同时创建了任务,也就是流水线运输和处理的内容。 自然我们可以在流水线中知道此流水线的名字。
实现Runnable接口 的方法,实际是创建了一个任务,我们还需要创建流水线,去运行此任务。 我们不能根据任务就知道运行此任务的流水线名字,因为可能有多个流水线执行此任务,所以我们要先获取正在执行此任务的流水线,这样就能知道正在运行的流水线名了。
实现Callable接口 的方法,此方法就可以用FutureTask这个员工来获得流水线处理任务后的结果(返回值),弥补了上面两种方法的不足。
这样解释是不是很好理解呢?当然这只是通俗的解释一下,方便理解,具体原理肯定还是要看代码的。
可以根据每种方法的优缺点来进行选择使用。
Thread 中的各种方法
- setName() 设置名字 getName() 获取名字
默认是Thread-序号,Thread本身构造方法也可传入名字来设置,可以在继承它的子类构造方法中利用super来调用。 - static Thread currentThread() 静态的,获取当前正在执行的线程的对象
jvm虚拟机启动后,会自动调用多个线程,其中就包含main来执行main方法中的代码。 - static void sleep(long time) 哪条线程执行到此方法,就会停留对应时间,单位毫秒ms
睡眠过后对于线程或被自动唤醒。
1、2两个方法在介绍线程创建时已经展示过,这里展示一下sleep方法的使用。
package com.study.threaddemo4;
public class Test4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("开始");
Thread.sleep(5000);
System.out.println("结束");
}
}
运行结果:
“开始” 打印后,main线程睡眠5s, 然后自动唤醒后,打印"结束"。
内部细节
线程栈之间是相互独立的,堆内的数据是共享的。
并发编程艺术
核心思想
写后读:一个线程等待其他线程写之后再去读取内容,然后再去写操作。
所有情况下,保证如上,都是正确的。
在完成写操作之后再把锁释放。
并发编程的挑战
线程不是越多越好,上下文切换是有时间开销的。
当计算量相同时,单核情况下,并发会有切换,所以会慢。
时间片
linux时间片相对固定,而windows是不固定会变化的。
多线程使用场景:cpu产生计算浪费比例较大的时候。
用多少个线程,由浪费比例决定。只能大概算一个上限,下限需要去测试。
分析线程工具
如何减少上下文切换?
使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
一次上下文切换大概1ms。
dump快照:导出这一瞬间的执行信息,形成一个文件。
命令(面试重点)
grep 筛选
awk 统计(重点)
sort 排序
uniq -c 去重 c: count
synchronized
synchronized(A) 锁代码块时,如参只能是基本类型。
不允许两个线程对A加锁,访问是可以的。
锁是加在对象头上的
锁代码块,等代码块执行完毕才释放锁。
锁方法,等方法执行完释放锁。
方法加上锁,任何一个栈想要拷贝此方法都要先加上锁。
也就是说同时只能有一个线程执行此方法。
静态加锁方法,任何一个被锁住,其他的静态方法也不允许被拷贝调用。
一定要是写完之后释放锁。
避免死锁方法(面试)
死锁:线程t1和线程t2互相等待对方释放锁
图:
避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
并发的底层实现
流水线计算
减少切换动态电路切换频率,减少了cpu调度。 所以会打乱执行顺序。
导致指令重排序问题。
整体是利大于弊的。
指令重排序
计算机执行顺序不确定。
比如创建对象,可能先new再生成句柄,可能先生成句柄再new,可以先指向,再生成句柄。
导致我们读取的不一定是最新的。
这时候就需要volatile技术了。
volatile
- 只能保证读正确,写不可以。
- 防止指令重排序。保证可见行。
当一个线程对数据修改时,另一个线程可以准确的读取它的改变。
final 也有一定的指令重排序左右。
术语
内存屏障:对于内存上锁,不让别的线程访问。
缓冲行:高速缓冲的存储单位。 64B。
原子性:不可中断的一系列操作,要么都成功,要么都失败。
缓冲行填充:一定的缓存行的填充是可以用空间换速度的。 例如布尔类型的底层是int而不是char,防止堵塞。
写命中:高速缓存中有这个数据,没有的话需要重新读取。 有就是写命中。
写缺失:一个有效的缓存行被写到不存在的内存中。
volatile
将当前处理器缓存行的数据写回到系统内存。
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
但是已经完成排队回传的不会被标记。
重排序规则:
CAS(比较并且交换)
只能靠java底层封装的方法,我们自己做不到。需要总线的锁。
synchronized 实现原理(面试重点)
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
synchronized用的锁是存在Java对象头里的。
java对象头
对象头分为三个部分,锁信息在第一部分。
锁的升级与对比
锁就算一个约定,加一个标记变量。
加锁的影响不到不加锁的。
静态锁影响不到非静态的。
持有锁的线程没有执行完,时间片就到了,这时候仍然持有锁。
多线程加锁竞争失败要进阻塞队列,不然会浪费cpu。
因为如果还在就绪队列,就可能会选中,但是竞争失败的没有对其的锁,就算被选中也是浪费cpu。
在竞争很小时(几乎不存在时),加锁和解锁已经唤醒阻塞队列就有点多余了。
这就产生的锁的升级,并发程度从低到高,不可逆。
偏向锁
栈帧就是方法。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
因为一个线程方法可能也会同时调用多次。
第一次 加完锁时间片运行完后,仍不释放锁。
第二次调度,也不用再加锁和释放锁。 节省了很多时间。
CAS加锁(比较且交换)
对象头已经记录被某线程加锁,就不能再访问了。 但仍会有问题。
所以往回输入的时候,需要用cas比较一下,再进行交换。
计算机底层很多都是cas加锁。
轻量级锁
t1已经对对象加锁,t2一直死循环尝试CAS对其加锁。
t2不进阻塞队列。这样速度会很快。
适用于t1加锁完, 很快执行完了。
自旋:快速感知,当其他线程释放锁,可以感知到,加锁。
但是,如果线程执行时间长,自旋的线程较多,会很消耗cpu.
重量级锁
不自旋,放入阻塞队列,不浪费cpu。
原子操作
要么全做,要么全不做。
两种实现方法:锁总线,锁和有关的缓存行。
线程安全类,可以直接拿来用。
如:Concurrentxxx、Atomicxxx
最常用的就是ConcurrentHashMap
ABA 问题
保存之前读取的数,进行修改后,与原值进行对比,对比成功,才交换。这也是CAS操作。
如果两个线程,其中一个线程两次修改,让其变成原值了,那么就会出现问题。
要解决ABA问题,那么就加上版本号,不用原值对比,用版本号,每次被读取,版本号加一。
Java 内存模型
线程之间如何通信?进程之间如何通信?
两种:共享内存和消息传递。
内存屏障
最后一个可以代表前三个。
单线程的写后读,是为了让其他线程准确读到。
和多线程的写后读不同。
happens-before
- A happens-before B。
- B happens-before C。
- A happens-before C。
重排序
数据依赖
上下有依赖,不允许指令重排序。如:
as-if-serial语义
重排序是有的,但是不改变执行结果。
事务
一整件事,要么都成功,要么都失败。
具有原子性的。
电压信号限制,相互叠加,不能同时传输
volatile的内存语义
volatile相邻的上下两行受到影响,防止重排序。
锁的内存语义
lock(面试大重点)
与synchronized相似,但是可以自己控制锁范围。
lock.lock() 加锁
lock.unlock() 解锁
底层队列同步类AQS的同步类。
非公平锁性能更好。(对比与公平锁)
非公平不包含
公平:是第一个线程的才能进行cas加锁。
非公平: 只需要cas成功即可。不包含isFirst(current)的代码
如果十个相同的方法,对资源加锁,一个方法释放锁不要解除所有的锁。
锁的重入(重入锁)
单一线程可以对多个持有锁的方法多次调用多次加锁。
如递归调用,每次执行完,释放一个。
state 表示加锁,解锁次数。
像学过的synchronized。
有竞争才用cas(说明有并发)
线程安全的类有那些?current.
final可以防止指令重排序。
finnal 和 volatile
finnal 较轻,速度稍微快一点。
但不如volatile功能那么全。
加锁和解锁
加锁:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的开始,首先读volatile变量state
if (c == 0) {
if (isFirst(current) &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
解锁:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 释放锁的最后,写volatile变量state
return free;
}
双重检测锁定初始化(面试大重点)
单例模式:双重检验锁 单例模式对内存开销大,一开始就要加载到内存中。
- 构造方法私有是防止new对象。
- 获得对象通过方法,方法是静态的,因为没有对象呢,所有调用的Singleton肯定也是静态的。
- 懒加载:用的适合才产生对象,饿汉:类加载就创建对象。
第二个singletonnull拦截第二个开始进入阻塞队列的。
第一个singletonnull拦截后序进来的。
Singleton的为什么是volatile?
防止被创建后,还没传入singleton变量中。
内存可见性:及时准确知道其他内存的清空。
什么时候用多线程?
浪费比例超过百分之50。
操作系统不管java的优先级的设置,根本无用。
yield()让出cpu会有延迟,sleep是立即。
sleep(0) 沉睡0秒,为了让出cpu。
tomcat、网络爬虫、访问数据库
Java并发编程基础
线程状态
新建、就绪、运行、等待、阻塞、死亡。
处于等待状态的线程,如果没有唤醒,将一直处于等待状态。
- sleep() 只让出cpu
- wait() 让出cpu 并 释放锁。所以一定要事先拥有锁。
一定进入到就绪态才能运行。
如何查看线程信息、如何进行java优化?
jps、jstack
启动线程
start() 进入就绪,不要理解错了。
中断
操作系统中断(面试点):
中断是为了响应,响应新的请求。
线程中断:
只有处于线程运行状态的才能被中断,不然中断不了。
线程间的通信(面试点)
共享变量,共享内存,管道,端口等。
通过join控制执行顺序(面试题)
package Thread01;
import java.util.concurrent.TimeUnit;
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread pre = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Domino(pre), String.valueOf(i));
thread.start();
pre = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "terminate.");
}
}
}
Threadlocal
通过拷贝,只能看见自己的。
set和get设置和取出值,begin(),end()获取时间
Threadlocal底层用了虚引用。
CountDownLatch
await() 等此进程执行完再向下执行。
countDown()
有countDown方法和await方法,CountDownLatch在初始化时,需要指定用给定一个整数作为计数器。当调用countDown方法时,计数器会被减1;当调用await方法时,如果计数器大于0时,线程会被阻塞,一直到计数器被countDown方法减到0时,线程才会继续执行。计数器是无法重置的,当计数器被减到0时,调用await方法都会直接返回。
线程池
// 执行一个job,这个job要实现Runnable接口
void execute(Job job)
// 关闭线程池
void shutdown()
// 减少工作者线程
void removeWorker()
// 得到正在等待执行的任务数量
int getJobSize()
长时间没有任务,回收撤销最大线程数。
优先放入核心线程数,若核心线程处理不完,放入队列中,若队列超过上限,开启最大线程数进行消耗。
第一个任务在第一个线程执行完,没有任务时,核心线程数也处于运行状态。
第二各任务会再创建一个进程去运行,而不是用第一个任务使用过后的线程。
核心线程开满了响应速度会快一些。
Java 中的锁
Lock 接口
锁的释放要放在finally中。
ReentrantLock(解决死锁)
解决死锁
同步队列
线程按顺序执行。
读写锁
适用于读多于写的场景。
特性:(面试点)
ReentrantReadWriteLock (面试大重点)
缓存:
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 获取一个key对应的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
// 清空所有的内容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
降级锁
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。
锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
condition 接口
唤醒部分锁,根据条件。
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以
实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等
待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
java并发编程 容器 和 框架
ConcurrentHashMap (面试必考)
面试题
- hashmap,hashtable,ConcurrentHashMap作对比
- ConcurrentHashMap实现原理
- hashmap实现原理
- ConcurrentHashMap实现原理(jdk1.7和1.8)
- hashmap实现原理(jdk1.7和1.8)
- 并且hashmap的扩容会导致哪些问题
hash
散列,随机分布,但是相对均匀。
java中队列都有那些种类?
四重,七个队列
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
Java中13个原子类
-
原子更新基本类型
AtomicBoolean:原子更新布尔类型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新长整型。 -
原子更新数组
AtomicIntegerArray:原子更新整型数组里的元素。
AtomicLongArray:原子更新长整型数组里的元素。
AtomicReferenceArray:原子更新引用类型数组里的元素。 -
原子更新引用类型
AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记位的引用类型。 -
原子更新属性
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
java中的并发工具类
CountDownLatch
某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1
countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
CyclicBarrier
同步屏障,可以重铸。
semaphore 型号量
java中的线程池
好处
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池,必须对其实现原理了如指掌。
线程池 参数
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线
程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任
务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,
线程池会提前创建并启动所有基本线程。 - runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几
个阻塞队列。
·ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原
则对元素进行排序。
·LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通
常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
·SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用
移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工
厂方法Executors.newCachedThreadPool使用了这个队列。
·PriorityBlockingQueue:一个具有优先级的无限阻塞队列。 - maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并
且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如
果使用了无界的任务队列这个参数就没什么效果。 - ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设
置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线
程设置有意义的名字,代码如下。
new ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build(); - RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状
态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法
处理新任务时抛出异常。
线程池线程个数配置
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的
线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配
置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务
和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量
将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过
Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
饱和策略(面试点)
·AbortPolicy:直接抛出异常。
·CallerRunsPolicy:只用调用者所在线程来运行任务。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。
Executor 框架
ThreadPoolExecutor
ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的
ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool
FixedThreadPool
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场
景,它适用于负载比较重的服务器。
SingleThreadExecutor
SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多
个线程是活动的应用场景。
CachedThreadPool(最常用)
CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者
是负载较轻的服务器
Runnable Callable 区别
Callable 有返回值
Runnable Callable的使用
待查询。
java 并发编程实践
熟悉命令。
网络编程
三要素
- ip:设备在网络上的地址,唯一标识。
- 端口号:应用程序在设备上的标识。
- 协议:数据在网络中的传输规则约定。
如:UDP、TCP、http 等。
ip
分类:IPv4、IPv6
IPv4:32为地址长度,分成4组。已经用完了。
IPv6:128位地址长度,分成8组。
IPv4
分类:公用地址(万维网) 和 私有地址 (局域网)
127.0.0.1 localhost 本机ip,永远寻找本机。
局域网ip在换网时,可能会发生变化。自己练习写127.0.0.1即可。
一些常用的命令:
ipconfig 查看本机ip
ping 检查网络是否连通,也可以ping网址,网站其实也是ip。
inetAddress 类
package com.lpy.socketdemo1;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class MyInetAddress {
public static void main(String[] args) throws UnknownHostException {
// 获取InetAddress对象,可以看作为一台电脑的对象, 名字 + ip
InetAddress address = InetAddress.getByName("DESKTOP-Q6NQ9GN");
System.out.println(address); // DESKTOP-Q6NQ9GN/192.168.137.1
// 获取 名字
String name = address.getHostName();
System.out.println(name); // DESKTOP-Q6NQ9GN
// 获取 ip
String ip = address.getHostAddress();
System.out.println(ip); // 192.168.137.1
}
}
端口号
由两个字节表示的整数,取值0~65535。
0 ~ 1023 端口号用于一些知名的网络服务或应用。
我们使用1024以上端口号。
一个端口号只能一个应用使用。
协议
协议:计算机网络中,连接和通信的规则被称为网络通信协议。
UDP 与 TCP
UDP 通信编程
发送 数据
- 创建 DatagramSocket 对象(创办 快递公司)
不传参,随机一个可用端口,传参,可指定端口。(发送端口) - 创建 DatagramPacket 对象(打包 需要发送的快递)
需传入指定的byte数组,长度,要发往的地址,接收的端口号。和我们现实中也很像,快递单上的信息。 - 发送数据(快递公司发快递)
- 关闭(关闭快递公司)
例子:
package com.lpy.socketdemo2;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
public class SendDemo {
public static void main(String[] args) throws IOException {
// 创建 DatagramSocket 对象
DatagramSocket ds = new DatagramSocket();
String str = "hello world";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 80;
// 创建 DatagramPacket 对象
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// 发送数据
ds.send(dp);
// 关闭
ds.close();
}
}
接收 数据
- 创建 DatagramSocket 对象,指定接收用的端口号(创建快递公司)
- 创建 DatagramPacket 对象,用byte数组接收,指定接收长度(准备一个包,去装快递)
- 接收数据(快递公司接收快递)
- 解析数据(打开包裹)
package com.lpy.socketdemo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class ReceiveDemo {
public static void main(String[] args) throws IOException {
// 创建 DatagramSocket 对象,指定接收用的端口号
DatagramSocket ds = new DatagramSocket(10086);
byte[] bytes = new byte[1024];
// 创建 DatagramPacket 对象,用byte数组接收,指定接收长度
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
// 接收数据
ds.receive(dp);
// 解析数据
byte[] data = dp.getData();
int len = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();
System.out.println("接收到的数据" + new String(data, 0, len)); // // 接收到的数据hello world
System.out.println("该数据是从" + address + "这台电脑中的" + port + "端口发送出来的"); // // 该数据是从/127.0.0.1这台电脑中的65220端口发送出来的
// 关闭
ds.close();
}
}
实现简易的通信聊天
send端:
package com.lpy.socketdemo4;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Send {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
Scanner sc = new Scanner(System.in);
int port = 80;
InetAddress address = InetAddress.getByName("127.0.0.1");
while (true) {
System.out.println("请输入发送的内容:");
String str = sc.nextLine();
byte[] bytes = str.getBytes("UTF-8");
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
ds.send(dp);
if (str.equals("bye")) break;
}
ds.close();
}
}
receive端:
package com.lpy.socketdemo4;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class receive {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(80);
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
while (true) {
ds.receive(dp);
byte[] data = dp.getData();
int len = dp.getLength();
String str = new String(data, 0, len, "UTF-8");
System.out.println("由" + dp.getAddress().getHostAddress() + "主机" + dp.getPort() +"端口号发来了:");
System.out.println(str);
if (str.equals("bye")) {
break;
}
}
ds.close();
}
}
可通过如下设置,就可运行多个send类发送数据。
三种通信 方式
- 单播(一对一)
- 组播(给一组电脑发送)
- 广播(给局域网中所有电脑)
组播广播与单播代码类似。
组播:用MuticaseSocket创建,同时接收时,接收时将本机ip加入到对应组中。
广播:仅仅是ip改成255.255.255.255即可。
TCP 通信编程
TCP:是一种可靠的网络协议,再通信两端都建立一个Socket对象。
通信之前要保证连接已经建立。
通过Socket产生IO流进行通信。
创建对象时,会连接服务器,连接不上,会报错。
所以,先运行服务端,再运行发送端。
发送数据
package demo1;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class Client {
public static void main(String[] args) throws IOException {
// 创建连接通道
Socket socket = new Socket("127.0.0.1", 1000);
// 通过通道获取输出流
OutputStream os = socket.getOutputStream();
// 输出内容
os.write("你好阿".getBytes());
// 释放资源
os.close();
}
}
接收数据
package demo1;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// 创建对象接收
ServerSocket ss = new ServerSocket(1000);
// 获取连接的通道
Socket socket = ss.accept();
// 根据通道获得输入流
InputStream is = socket.getInputStream();
// 字节流 转换为 字符流,再包装成缓冲字符流,不出现乱码,并 提高效率
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 读入
String line;
while ((line = br.readLine()) != null) {
System.out.print(line);
}
// 释放资源
socket.close();
ss.close();
}
}
三次握手,四次挥手
传输文件 并 回馈
Client:
package demo3;
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
// 建立连接
Socket socket = new Socket("127.0.0.1", 1000);
// 创建缓冲输入字节流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("start/img.jpg"));
// 创建输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
// 读入文件并传输
byte[] bytes = new byte[4096];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
// 写出结束标记
socket.shutdownOutput();
// 获取服务器回馈,读入,字节流转字符流,在转缓冲流
BufferedReader br= new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
line = br.readLine();
System.out.println(line);
// 释放资源
socket.close();
}
}
Server:
package demo3;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// 创建对象
ServerSocket ss = new ServerSocket(1000);
// 接收连接
Socket socket = ss.accept();
// 读取数据,并保存到本地
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("target/b.jpg"));
byte[] bytes = new byte[4096];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
// 写回数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.flush();
// 释放资源
socket.close();
ss.close();
}
}
运行结果:回馈和传输成功