Java入门
-
Java由Sun公司创造,前身是Oak
-
Java开发类型
- javase Java单机应用
- j2me 移动端开发
- j2ee 网站,小程序或app的后台
- 安卓开发 kotlin/java
-
JVM,JDK和JRE
-
JVM Java Virtual Matchine —>Java虚拟机
-
JDK Java Development Kit —>Java开发包
-
JRE Java Runtime Environment —>Java运行环境
-
)
-
Java的特性
- 一次编译,处处执行
- 这体现了Java的可扩展性和可执行性
- Java根据当前操作系统的不同,将程序编译成不同操作系统所适配的字节码文件
- 一次编译,处处执行
-
环境变量
- CMD
- CMD命令(控制台指令)
- 首先从当前目录开始查找指令文件,找不到就从系统变量path所定义的目录中查找,还找不到就报错
- Java常用指令
- javac java文件名.java
- java java文件名
- 环境变量的配置
- JAVA_HOME=JDK所在的目录
- PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;
- CLASSPATH=.;%JAVA_HOME%/lib;%JAVA_HOME%/lib/tools.jar;
- 注:classpath:配置的是java编译之后的产物class文件的查找范围
- class文件包含了操作系统执行所需要的二进制字节码
- CMD命令(控制台指令)
- CMD
Java数据类型与语法
Java命名规范
-
类名
- 大驼峰命名法:每个单词的首字母大写
-
变量名及方法名
- 小驼峰命名法:第一个单词首字母小写,其他单词首字母大写
-
强制规则
- 变量名只能由数组,字母,_,$组成
- 不能以数字作为变量的开头
- 不建议以中文或纯$或纯_作为变量名
Java数据类型
-
基本数据类型[字面量后面跟着l,f表示不按照int/double类型而是long/float类型定义]
-
整型(默认0,字面量默认数据类型int)
- byte 一个字节
- 第一位作为符号位
- -27~27-1
- short 两个字节
- -215 ~215-1
- int 四个字节
- -231 ~231-1
- long 八个字节
- -263 ~263-1
- byte 一个字节
-
浮点型(默认0.0,字面量默认数据类型double)
小数=位数+精度 所有的浮点型所使用的都是二进制科学计数法 浮点数的计算本质是实数运算,但计算机只存储整数,所有的实数都是约数,会导致浮点型的运算很慢且有误差 银行对数据敏感,小数一般会用BigDecimal
- float 32位
- 单精度浮点数
- 第一位为符号位,指数部分8位,位数部分23位
- 范围大于int,精度小于int
- double 64位
- 双精度浮点数
- 第一位为符号位,指数部分11位,位数部分52位
- 范围大于long,精度小于long
- float 32位
-
布尔型
- boolean
- true/false
- boolean
-
字符型
计算机本身不存储文字本身,而是存储文字对应的字符集编码 最早来自于莫尔斯码 中文编码gbk2312 unicode万国码 utf-8英文1字节中文2字节 utf8mb4拓展编码拓展了emoji
- char
- 字符集ASCII—用于存储一个字符
- 用单引号
- char
-
-
引用数据类型
-
数组
-
一维数组
//数组的声明 int[] a=new int[11]; //数组的初始化 //1.静态初始化 在数组声明时就确定每个元素的值,数组长度根据提供的元素数量自动计算 int[] arr={1,2,3} //2.动态初始化 不给元素提供值,所有数组元素被创建时初始化为数组数据类型的默认值;引用类型与数组类型必须一致 int[] a=new int[11]; a[0]=1; //数组遍历 //1.普通for循环遍历 for(int i=0;i<arr.length;i++){ System.out.println(arr[i]); } //2.增强for循环遍历 //对于数组而言,增强for编译时会转换成普通for循环进行遍历,集合的遍历会转换成迭代器遍历 for(int e:arr){ //e此时不代表下标,而是用于临时存储数组元素的变量(容器) //无需开发者关心数组遍历的次数,开发者只需要提供要遍历的数组和遍历时所使用的临时变量就可以实现遍历数组 System.out.println(e); }
-
二维数组:本质是多个一维数组组成
//静态初始化 int[][] arr={{},{}}; //动态初始化 int[][] arr=new int[3][]; arr[1]=new int[4]; int[][] arr=new int[3][3] //遍历 for(){ for(){ } }
-
补充
1.声明数组时必须同时提供数组的容量 a.在java中,new会在堆内存中申请一块空间用于后续对象数据的存放 b.数组的容量在创建时就已经确定了,不能修改,如果长度变化,一定是一个新的数组 2.数组被创建时,所有元素都会被默认初始化,初始化的值与数组的类型有关 整型0;浮点型0.0;布尔型false;char类型为空字符(\u0000);所有其他类型(引用类型)为NULL 3.[]代表的是数组的下标,即索引(基0) a.数组中的数据在内存中连续存储 b.最多存储到arr.length-1 c.通过下标,可以直接访问或修改数组指定元素的值 4.ava中允许多个引用存储同一个对象的内存地址,但是不允许一个引用存储多个对象的内存地址 a.若某个对象没有被任何引用存储内存地址,这个对象会被标记,后续被gc垃圾回收器回收内存空间
-
-
字符串
- 用双引号
-
对象
-
-
数据类型的转换
-
自动类型转换
- 小转大,几乎没有风险
- 会由于精度不一致出现转换过程中的精度缺失
-
强制类型转换
- 大转小,可能出现数据溢出的情况
-
Java运算符
-
算术运算符
+ - * / % ++ --
-
逻辑运算符
& | ! && || (短路运算符) && 短路与 ||短路或 即若左边的运算结果已经能决定整个运算结果,则表达式右边的代码不会执行 &两边都为true才为true,否则为false |两边都为flase才为false,否则true
-
关系运算符
> < == >= <= !=
-
位运算
&按位与 参与运算的两个值,逐位进行与运算,如果两个值都是1,则返回1,否则返回0 |按位或 参与运算的两个值,逐位进行或运算,如果两个值都是0,则返回0,否则返回1 ^按位异或 参与运算的两个值,逐位进行异或,若相同返回0,不同返回1 ~按位取反 符号位也会取反,按位进行取反,原本为0取1,原本为1取0 <<左移 二进制左移 >>右移 二进制右移
-
赋值运算符
= 右运算符,先运算等式的右边,再赋值等式的左边
-
条件运算符(类if-else)
a?b:c a为true返回b,否则返回c[b与c可以是三元表达式,一般来说三元嵌套不超过三层]
Java分支结构
-
顺序结构 :代码自上而下,自左向右依次运行
-
分支结构
switch(){ case 1: break; default: break; } if(){ }else if(){ }else{ }
-
循环结构
while(){ } do{ }while() for(;;){ } 第二个值产生一个布尔值,根据布尔值判断循环是否继续运行;第一个是变量的声明赋值部分,只会在循环的第一个运行时执行
-
break与continue
-
break:跳出当层循环
-
continue:跳过本次循环的这一步
-
可以通过给循环命名,退出指定的循环
loop:while(){ break loop; }
-
Java面向对象程序设计
面向对象与面向过程程序设计
- 面向过程
- 面向对象:用户无需关心系统实现的细节,只需要关注系统功能的结果
面向对象基本概念
-
类与对象
- 类:分类
- 对象:类的实例
-
成员变量
- 定义在类中,为所有对象共享
- 每一个对象的成员变量的值是不同的
- 和其他变量一样,需要在声明时定义数据类型
-
局部变量
- 方法中定义的变量
- 每个局部变量只在变量所在的花括号内有效
-
方法
-
特别的,通过对象调用的方法叫做成员方法
-
方法的调用:引用名.方法名(参数)
-
方法的重载(编译时多态)
- 方法在参数不同的情况下,方法名可以重复,在调用方法时,根据参数不同调用不同的方法
- 参数的不同
- 参数的类型
- 参数的数量
- 参数的顺序
-
方法的组成
public 返回类型 方法名(参数){ //为了解决方法之间数据通信的问题,引入了方法的参数,基本数据类型是值传递 //方法体 return 返回值; //如果要在方法中返回值,则需要定义返回值的数据类型 }
-
-
面向对象的特点
- 封装:将对象的属性和方法的实现细节隐藏,只对外提供公共的访问方法
- 封装的类型:
- 类
- 方法
- JavaBean:封装类
- javaBean的三大特点
- 所有属性私有
- 所有属性提供共有的setter/getter方法
- 所有类必须显示地定义无参构造器
- private
- 可以用于修饰成员变量,方法,甚至是类(内部类)
- 被private修饰的元素,只能在当前类中访问,其他类无法访问
- 布尔值的成员变量必须遵循isXX()的命名方式
- javaBean提供了对象构建的一种规范,要求所有程序员都遵循javaBean要求的定义和使用类
- Lombok插件
- 注释[自动生成get/set/构造器]
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- 注释[自动生成get/set/构造器]
- javaBean的三大特点
- 封装的类型:
- 继承:子类自动拥有父类所有可继承的属性和方法
- 解决了代码复用性的问题
- 将子类中所有的共性定义在父类中,子类只需要直接继承就可以复用这些共性,子类可以在子类中定义自己的分类
- 父类的私有属性可以继承但无法直接访问,子类需要通过从父类继承的公有get/set方法进行访问
- Java是单继承的,一个类只能有一个直接子类,但一个类可以有多个子类
- 任何类的变量都会在构造函数之前加载,如果继承了父类,父类成员优先加载;先完成父类变量初始化,再调用父类无参构造函数,再完成子类成员变量初始化,再调用子类无参构造函数
- 多态:配合继承和方法重写,提高了代码的复用性和可拓展性,如果没有方法重写多态毫无意义
- 方法的重写(覆盖)
- 子类中出现和父类中相同方法名和参数的方法,通过子类对象调用方法的时候就不再会调用父类而是调用子类自己的方法,这种现象叫做方法的重写或覆盖
- @Override:告诉开发者这个方法重写(覆盖)了父类的方法,如果这个方法没有重写父类的方法无法通过编译
- 方法的重写(覆盖)
- 封装:将对象的属性和方法的实现细节隐藏,只对外提供公共的访问方法
-
this与super
-
this
//1.在子类自己的方法中,调用子类自己的方法 this.方法名(); //2.在子类自己的方法中,调用子类自己的成员变量 this.变量名; //3.在子类自己的构造方法中调用子类指定的构造方法 子类名(){ this(参数); }
-
super:借助父类的构造方法,完成对子类的属性的赋值
//1.在子类的成员方法中访问父类的成员方法 super.方法名(); //2.在子类的成员方法中访问父类的成员变量[通常用于访问静态变量] super.变量名; //3.在子类的构造方法中访问父类的构造方法[java中规定子类调用构造函数时会调用父类的构造函数] //super要在子类构造函数的第一行 //若构造子类的有参构造方法,默认还是会调用父类的无参构造方法(不写super(?)就由jvm默认生成) 子类名(){ super(参数); }
-
-
静态变量:由static修饰,为所有对象共享
面向对象编程
-
抽象类(is a)
-
作用
- 不提供方法的具体实现,具体实现交由子类自己实现
- 如果父类中定义了抽象方法,子类就必须重写父类所有的抽象方法,除非子类自己也是一个抽象类
-
abstract:修饰类是抽象类,修饰方法是抽象方法
-
特点
- 抽象类不可被实例化
- 抽象类定义的抽象方法需要由子类重写
- 抽象类中可以没有抽象方法,可以定义普通方法
- 抽象类中可以定义成员变量用来给子类继承
- 抽象类可以定义构造方法,但是也是用于给子类继承
- 如果子类没有重写父抽象类的方法,这个子类一定也是一个抽象类,否则无法通过编译
-
缺点:抽象类无法让java摆脱单继承的缺陷
-
-
接口(has a)
- java为了解决单继承限制的问题,创建了接口
- 特点
- 接口可以多继承
- 接口中不可以定义成员变量和构造方法,也不可以有非抽象方法
- 接口中所有方法都是有abstract和public修饰的
- 在jdk1.7之后,接口可以定义常量(static和final所修饰的变量)
- 在jdk1.8之后,接口可以包含静态方法和默认方法
- 在jdk9之后,可以在接口中定义私有方法
-
继承与实现
-
继承:强耦合
-
实现:弱耦合
-
接口与抽象类的区别
1.抽象类可以有构造方法,接口中不能有构造方法。 2.抽象类中可以有普通成员变量,接口中没有普通成员变量 3.抽象类中可以包含非抽象的普通方法,JDK1.8后接口中的可以有非抽象方法,比如默认方法和静态方法和私有方法 4.抽象类中的抽象方法的访问类型可以是public和protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。 5.抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。 6.一个类可以实现多个接口,但只能继承一个抽象类。
-
-
多态
-
对象的多种型态就叫做对象的多态性
-
多态的体现:父类引用可以指向子类对象
-
里氏代换原则:任何父类对象可以出现的地方,都可以用子类对象进行替换
-
动态代理:只需要提供接口,实现类自动生成
-
Object:超父类
-
动态绑定
- 通过父类引用指向不同的子类对象,达到调用不同子类方法的现象叫做动态绑定
- 动态绑定的前提
- 要有继承
- 要有方法重写
- 要有父类引用指向子类对象
-
多态的优点
- 可以保护底层代码的实现,对外只暴露接口,限制接口使用者能够调用的方法
- 拓展数组和集合数据的类型(存储数据的方式)
- 拓展方法的参数或返回值类型的范围
-
多态的弊端
-
多态的类型
-
向上转型:父类引用指向子类对象
- 向上转型几乎没有风险,但是子类对象所拥有的方法无法通过父类引用进行调用
- 将子类对象赋值给父类引用
- 应用场景:当不需要子类类型或使用父类功能就能够满足要求或不希望调用子类方法
-
向下转型:本质上是对象还原的行为
- 向下转型必须保证对象本身就是子类对象,如果不是子类对象,强制转换就会报错
- 应用场景:如果需要使用子类所特有方法的时候
-
-
instance of关键字
//判断引用所指向的对象是不是指定类的对象或指定类子类的对象 if(a instance of Object){ }
-
-
final关键字
- 修饰类,类不能被继承
- 修饰基本数据类型的变量,变量的值不可以被修改
- 修饰引用类型的变量,引用所存储的对象的地址不可以被修改
- 修饰方法,方法不能被重写
-
static关键字
-
修饰变量:静态成员变量
- 静态方法通过类调用,不需要通过对象进行调用
- 类名.方法名()
-
修饰方法:静态方法
-
修饰代码块:静态代码块
- 在类加载的时候执行且只执行一次
-
-
普通代码块
- 加载类的非静态变量时执行
-
类加载机制
-
编译
-
加载
- 在类被初始化时,类中的属性被分为静态属性和非静态属性
- 类在加载时会优先加载类的静态变量和静态代码块(如果类中有main方法,一定是所有静态属性中最后被加载的),然后再加载类的f非静态变量和非静态代码块(按照书写顺序从上到下)
- 类的构造函数最后被加载:不管是静态方法还是成员方法,加载是正常加载,但是只有被调用时才会执行,而代码块不需要显式调用,加载时就会显式执行
- 如果使用类时没有访问类的非静态属性,那么类的非静态属性就不会被加载
- 静态属性永远在非静态属性之前加载
- super和this不可以出现在静态属性中,同一个类中静态成员只能访问静态成员,而非静态成员可以访问静态成员
- 在访问静态成员变量和静态方法时不要通过引用访问,而是通过类访问
-
运行
-
结束
-
-
访问修饰符
public protected default private 同类中 √ √ √ √ 同包类 √ √ √ × 不同包的父子类 √ √ × × 不同包的无关类 √ × × × -
递归算法
-
案例:Fabonacci数列,求阶乘
-
优点:写法简单,清晰
-
缺点:
- 如果没有设置好递归的出口,容易出现错误
- 递归调用本身的性能消耗过大
-
注意事项:控制递归的次数,避免方法调用过多导致性能过差或者栈内存溢出
-
-
enum枚举类:替代最终静态常量
-
限制最终静态常量的范围
-
使用
public enum Season { SPRING, SUMMER, AUTUMN, WINTER } public static void main(String[] args) { switch (Season.valueOf("")) { case SPRING: System.out.println("春天"); break; default: System.out.println("不想输出"); } }
-
Java异常处理
异常的类型
-
Error:无法预知,无法解决的错误
- StackOverFlowError栈溢出异常
- OutOfMemoryError堆溢出异常
- 可以通过修改jvm参数解决该异常
- -xms:最小内存
- -xmx:最大内存
- 可以通过修改jvm参数解决该异常
-
Exception
- RuntimeException:代码运行时产生的异常,不是必须进行异常处理,但是后续的代码无法正常执行
- CheckedException:一般来说,异常产生的原因和开发者本身无关,大部分情况下由外部原因导致,jvm规定,有可能产生检查时异常的代码,无论是否发生异常,都必须进行异常处理,否则无法通过编译
异常处理
-
try-catch-fianally
try{ //一个try中有多个异常 //能确定类型就具体到类型 //不能确定使用父类类型 }catch(Exception e){ //捕获并处理后,后续代码可以继续执行 e.printStackTrace();//打印异常栈信息 }finally{ //finally中的代码无论是否抛出异常都会执行 }
-
throw:主动抛出一个异常对象
-
throws和throw的区别
1.throw出现在方法中,而throws出现在方法声明之后 2.throw会明确抛出一个异常对象,而throws只代表一种抛出异常的可能 3.两者都是消极的处理异常的方式 4.最终异常处理还是要交给try-catch-finally
自定义异常
- 继承Exception或Throwable抽象类
- 使用方法,直接throw new 自定义异常名();
Java常用类与常用Api
String,StringBuffer,String
-
String
-
不可变长字符串,原码中定义的是一个final类型的字符数组
-
方法区中定义了一个特殊的内存区域叫做运行时常量池,用于存放等号赋值创建的字符串对象
字符串比较 String str="abc" true String str=new String("abc") false String str=new String(new Byte[]{}) false String str=new String(new char[]{}) false String str=str1+str2+str3 false
-
常用方法
charAt 定位字符 concat 指定字符串连接到字符串末尾 contains 包含字符串 endswith equals String中判断当前字符串对象与参数的内容地址是否相同,相同返回true,否则进入下一个比较 接着判断参数存储的对象是否是string,若不是,直接返回false 向下参数转型,方便访问作为字符串的属性和方法 获取字符串长度,长度不同返回false 将两个字符串对于的char数组内存获取 将两个字符串对应的数组遍历逐位比较,一位不同返回false,遍历完成没有不同返回true 比较地址用==,比较字符串用equals equalsIngoreCase 比较忽略大小写 indexOf 字符串中匹配的字符串的起始下标 isEmpty 是否为空,当且仅当length为0 lastIndexOf 字符串中匹配的字符串的最后一个下标 Length() replace 字符串替换 split 字符串分割 startWith subString 字符串截取 substring() 从下标开始 substring(,)区间左闭右开 tocharArray 字符串转为新的字符数组 trim 去除字符串空格(左右)
-
-
StringBuilder
-
可变字符串
-
StringBuilder继承自AbstractStringBuilder,创建一个非final的字符数组,容量不够扩容(创建一个新的字符数组)
-
初始容量16[或者16+设定容量],每次扩容2*(n+1)
-
常用方法
append charAt capacity delete deleteCharAt ensureCapacity insert
-
-
StringBuffer
- 可变字符串
- 支持线程安全,牺牲了性能
-
String,StringBuilder和StringBuffer的区别
- StringBuilder>StringBuffer>>String
- long time=System.currentTimeMiles() 当前时间距离计算机时间原点所过去的毫秒数
数学计算Math
-
Math
Math.方法名()直接调用,Math类中都是静态方法 还有两个常量PI和E
-
常用方法
-
ceil:向上取整
-
floor:向下取整
-
pow:取幂
-
random:返回double
-
round:四舍五入
-
键盘输入Scanner
-
Scanner
Scanner sc = new Scanner(System.in); int x = sc.nextInt();
-
System.in负责从控制台获取用户键盘输入
-
推荐用nextLine默认获取String类型.如果需要使用其他类型做好类型转换即可
-
补充:nextInt后不能根nextLine
首先,Scanner是一个扫描器,它扫描数据都是去内存中一块缓冲区中进行扫描并读入数据的,而我们在控制台中输入的数据也都是被先存入缓冲区中等待扫描器的扫描读取。这个扫描器在扫描过程中判断停止的依据就是“空白符”,空格啊,回车啊什么的都算做是空白符。 nextInt()方法在扫描到空白符的时候会将前面的数据读取走,但会丢下空白符“\r”在缓冲区中,但是,nextLine()方法在扫描的时候会将扫描到的空白符一同清理掉。
随机数Random
-
Random
Random rand =new Random(); int x = rand.nextInt();
日期相关 Date,SimpleDateFormat,Calendar
-
Date日期类
Date date = new Date();//开发者没有指定日期的时间,默认取当前时间 //指定参数就视为距离1970/1/1/0所过去的毫秒数 //注意:@Deprecated 表示方法被弃用,不建议使用 //常用方法 getTime() 当前距离原点时间过去的毫秒数
-
SimpleDateFormat
SimpleDateFormat负责字符串和日期的互相转化 y表示年,M表示月,d表示日,H表示24小时制的小时,h表示12小时制的消失,m表示分钟.s表示秒,S表示毫秒 日期转字符串流程 1.需要在构造函数的参数中定义日期所应该具有的格式 2.准备一个日期类型的对象 3.使用format方法进行转换.注意第一步日期格式不能出错,否则会抛出异常 字符串转日期流程 1.创建sdf对象 2.创建字符串对象 要求字符串代表的日期与sdf对象的格式一致,否则会出现ParseException 3.将字符串转换为日期 常用方法 parse 字符串转日期对象 format 容器对象转字符串
-
Calendar日历类
Calendar提供相关方法负责做日历的翻页 默认情况下没有指定时间,就使用当前时间 使用单例模式,Calendar.getInstance获取对象 手动指定日期 1.创建日期对象 2.将日期时间设置到日历上 3.再获取当前日历时间 add c.add(Calendar.MONTH, 1); 日期回到下个月的今天 c.add(Calendar.YEAR, -1); c.add(Calendar.Month,-1); 常用方法 setTime 设定日历对应的日期,不设定默认当前 set 设置月份时0-11为1-12月 getTime 获取日历当前的日期 add(时间单位,数字) 指定单位的三下调整,+表未来,-表从前 set(时间单位,数字) 设置指定时间单位的值
封装类(包装类)[Integer,Byte,Short,Long,Double,Float,Boolean,Character]
-
封装类和基本数据类型
八种基本数据类型有八种对应的封装类 封装类提供了各种方法,通过对象调用方法的方式更好更方便地管理数据 封装类型和String接近,支持等号直接赋值 封装类中四种整型和String一样,支持运行时常量池技术 如果封装类用=直接赋值,且值的范围在-128~127中,存储在常量池中,并且支持复用,如果超出范围就存储在堆内存中,而不再存储在运行时常量池中 如果希望比较值,还是使用equals方法
-
Integer—int
int i=1; Integer integer = new Integer(1); int b=integer.intValue(); int c=integer; Integer d=1;
-
Byte—byte
-
Short—short
-
Long—long
-
Float—float
-
Double—double
-
Boolean—boolean
-
Character—char
Character.isLetterOrDigit(c)
-
-
拆箱与装箱
- 装箱:将基本数据类型封装为封装类型
- 拆箱:将封装类型还原为基本数据类型
集合
集合的分类
-
Collection:单列集合
List接口 特性 1.有序集合(存取顺序一致) 2.允许存储重复的元素 3.有索引,可以使用for循环进行遍历 Set接口 特性 1.不允许存储重复元素 2.没有所有,无法通过for循环遍历 3.不保证有序
-
Map:双列集合
-
泛型:用于限制集合中存储元素的数据类型
- JVM在编译时会检查集合中元素的数据类型和泛型中定义的是否一致
- 嵌套的泛型:存放的是集合的内存地址
- 所有集合创建时必须提供泛型,不提供默认Object
- 泛型类型(动态类型)
- 创建类在类名后跟,在创建对象的时候指明该泛型类型的具体类型
类名<具体类型> 对象名=new 类名<>(); - 泛型类型不再依赖于父类类型本身
- 创建类在类名后跟,在创建对象的时候指明该泛型类型的具体类型
-
迭代器
-
使用迭代器时若要删除元素,必须调用迭代器的remove方法,而不是遍历集合本身的remove方法
- 使用迭代器的remove,虽然还是调用集合的remove,但是相关的cursor和lastret等变量会同步更新,就不会出现遍历时的下标越界异常
-
modcount:用于与预期修改次数对比,判断是否在迭代器外进行了集合操作
-
ConcurrentModificationException:使用集合本身的remove,modCount++,这时迭代器检查,发现modCount和exceptedModCount不一致,说明集合在迭代器不知情的情况下对元素删除,迭代器中的cursor和lastRet失效,导致next方法访问时访问到了不存在的元素,从而报错
- 迭代陷阱:使用迭代器或增强for时,必须使用迭代器本身的remove方法
-
所有集合的对象都重写了toString方法
- 都是使用迭代器进行遍历
- 都是使用StringBuilder进行拼接
-
-
Collections工具类
reverse 反转 shuffle 混淆 sort 排序 swap 交换 rotate 滚动 synchronizedList 线程安全化
-
集合与数组的区别?
- 集合的长度是可变的,数组的长度是固定的
- 集合存储的都是对象,而且对象的类型可以不一致,在开发中一般当对象多的时候,我们使用集合进行存储.
-
Queue: 先进先出队列
Deque: 双向链表
Collection和Map之间没有关系,Collection是放一个一个对象的,Map 是放键值对的
Deque 继承 Queue,间接的继承了 Collection
Collection
Collection接口
-
Collection是 Set List Queue和 Deque的接口
-
单列集合的遍历
-
for循环(只适用于List)
for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
-
增强for循环
for (Object s:list) { System.out.println(list.get(i)); }
-
迭代器
Iterator<> it=list.iterator() while(it.hasNext()){ Object o=it.next(); }
-
-
常用方法
这些方法是在上层接口中定义的,所有的实现类都可以使用这些方法 size方法 集合中元素的个数 isEmpty 是否为空 contains 包含 toArray 转为数组 add 添加元素 remove 删除元素 containsAll 是否包含了一个集合的所有元素 addAll 将两个集合取并集 removeAll 差集 clear 清空集合 equals 比较两个集合
-
注意:下标不能超出当前集合元素下标的范围
List接口
-
List接口
常用方法 get 根据下标获取元素 set 指定下标设定元素 indexOf 返回下标,没有就-1 lastIndexOf 返回最后一个下标,没有就-1 subList 截取指定下标元素返回新集合
-
常用实现类
List常用实现类 ArrayList 底层数组 连续存储 LinkedList 底层单向链表 支持索引;链表查找时性能相对较慢,增删的时候性能表现较好 常用方法 addLast addFirst getFirst getLast removeFirst removeLast LinkedList 除了实现了List和Deque外,还实现了Queue接口(队列)。 Queue是先进先出队列 FIFO,常用方法: offer 在最后添加元素 poll 取出第一个元素 peek 查看第一个元素 element检索,但不删除 父接口Deque add添加 offerFirst/Last入队 pollFirst 出队 pollLast element 检索,但不删除 peekFirst检索但不删除此队列的头部,如果此队列为空,则返回 null 。 peekLast Vector 底层数组,线程安全,但是效率差
Set接口
-
Set接口
- 没有get方法,无法通过下标获取元素
- 只能通过增强for或迭代器遍历
-
Set常用实现类
- HashSet:底层存储结构是哈希表
- TreeSet:底层是红黑树
- TreeSet默认会对存储的数据进行排序.使用时要么泛型类型实现了比较器,要么再创建TreeSet对象时提供了比较器对象作为参数否则无法使用
-
自定义排序
-
如果集合泛型对应一个引用类型,必须实现比较器才可以进行排序
-
实现方式
方式一:本身是强耦合,一旦后续要重写排序规则就要修改原类 Comparable接口(强耦合) 要排序的对象实现Comparable接口,重写compareTo方法(+1升序,0相同,-1降序) 方式二:弱耦合,可以根据使用条件动态切换使用条件 Comparator 创建比较器类,实现Comparator接口 将比较器对象作为参数传入
-
Map
-
Map接口:通过键值对存放数据
- key不可以重复,value可以重复
- Map中有一个内部接口Entry
-
Map常用方法
常用方法 containsKey containsValue size isEmpty get put remove putAll clear keySet values
-
Map的遍历
-
keySet遍历
1.迭代器 Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()){ String key=iterator.next(); System.out.println(key+":"+map.get(key)); } 2.增强for for (String key:map.keySet()) { System.out.println(key + ":" + map.get(key)); }
-
entrySet遍历
1.迭代器 Iterator<Map.Entry<String, String>> iterator1 = map.entrySet().iterator(); while (iterator1.hasNext()) { Map.Entry<String, String> next = iterator1.next(); System.out.println(next.getKey() + ":" + next.getValue()); } 2.增强for for (Map.Entry<String,String> entry:map.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); }
-
-
Map常用实现类
-
HashMap
- key和value都允许为null,但不推荐使用
- 线程不安全,性能相对较好
-
HashTable
- 被弃用,支持线程安全,性能稍差
- Key和value不可以为null
-
TreeMap
-
类似于TreeSet,使用TreeMap,对应的key所对应的对象必须实现比较器或提供实现比较器对象作为参数,再使用TreeMap时按照key的大小进行
-
常用方法
常用方法 firstKey lastKey firstEntry lastEntry higherKey往上取最近的一个(不是本身) ceillingKey往上取最近的一个(包含本身) ceillingEntry higherEntry lowerKey floorKey floorEntry lowerEntry
-
-
-
手撕HashMap
/** *HashMap接口 */ public interface FakeMap<K, V> { V put(K key, V value); V get(K key); void clear(); int size(); boolean containsKey(K key); boolean containsValue(V value); Set<Entry<K, V>> entrySet(); Collection<V> values(); V remove(K key); void putAll(FakeMap<K, V> m); Set<K> keySet(); String toString(); } /** *Map.Entry */ @Data @NoArgsConstructor @AllArgsConstructor public class Entry<K, V> { private K key; private V value; } /** *FakeHashMap */ public class FakeHashMap<K, V> implements FakeMap<K, V> { //声明一个数组,用于保存键值对信息 //这个数组再FakeHashMap被实例化时,默认自己完成实例化 //这里不在考虑数组扩容问题,方便将hashcode对应的数组下标 //将数组的长度默认设为2000 LinkedList<Entry<K, V>>[] list = new LinkedList[2000]; //记录当前map中所有已经被使用的hashcode Set<Integer> hashSet = new HashSet<>(); //真实的HashMap的hashcode由key对应的类自己提供,但由于我们数组长度固定 //需要保证再计算出来的hashcode值必须在数组的下标范围内 //这里借用String的hashcode值进行计算,并对数据 //长度积极性取余,确保hashcode能在list数组的下标范围内 public int hashcode(K key) { return key.toString().hashCode() % 2000; } @Override public V put(K key, V value) { //先计算K对应的hashcode值 int h = hashcode(key); //找到hashcode值作为下标在List数组中对应的链表 //并且判断这个链表是否存在 hashSet.add(h); if (list[h] == null) { //说明这个index作为数组下标第一次使用,绑定一个List list[h] = new LinkedList<Entry<K, V>>(); //将hashcode值进行记录 hashSet.add(h); //直接存入键值对对象 Entry entry = new Entry(key, value); list[h].add(entry); return value; } else { //如果不为空,分为两种情况 //1.存入的键值对和链表中键值对key没有相同,则直接存入 //2.存在key重复,则应该不新建键值对,而是将新的value替换原有的value for (Entry<K, V> entry : list[h]) { if (key.equals(entry.getKey())) { entry.setValue(value); return value; } } list[h].add(new Entry<>(key, value)); hashSet.add(h); } return value; } @Override public V get(K key) { //计算key对应的hashcode值 int h = hashcode(key); //获取h对应的链表,为null返回null if (list[h] == null) { return null; } else { for (Entry<K, V> entry : list[h]) { if (entry.getKey().equals(key)) { return entry.getValue(); } } } return null; } @Override public void clear() { //将所有已经被使用的数组下标置为null,并且清空HashSet for (Integer h : hashSet) { list[h] = null; } hashSet.clear(); } @Override public int size() { int count = 0; for (Integer h : hashSet) { count += list[h].size(); } return count; } @Override public boolean containsKey(K key) { if (hashSet.size() == 0 || key == null) { return false; } int h = hashcode(key); if (!hashSet.contains(h)) { return false; } else { for (Entry<K, V> entry : list[h]) { if (entry.getKey().equals(key)) { return true; } } } return false; } @Override public boolean containsValue(V value) { if (hashSet.size() == 0 || value == null) { return false; } for (Integer h : hashSet) { for (Entry<K, V> entry : list[h]) { if (entry.getValue().equals(value)) { return true; } } } return false; } @Override public Set<Entry<K, V>> entrySet() { Set<Entry<K, V>> entrySet = new HashSet<>(); if (hashSet.size() == 0) { return null; } for (Integer h : hashSet) { for (Entry<K, V> entry : list[h]) { entrySet.add(entry); } } return entrySet; } @Override public Collection<V> values() { Collection<V> values = new LinkedList<>(); if (hashSet.size() == 0) { return null; } for (Integer h : hashSet) { for (Entry<K, V> entry : list[h]) { values.add(entry.getValue()); } } return values; } @Override public V remove(K key) { //删除的时候,如果键值对所在的链表还有其他键值对,什么都不做 //如果所在链表size为0,从hashSet将当前链表对应的hashcode移出 int h = hashcode(key); if (!hashSet.contains(h)) { return null; } else { if (list[h].size() > 1) { return null; } else { Entry<K, V> entry = list[h].get(0); list[h] = null; hashSet.remove(h); return entry.getValue(); } } } @Override public void putAll(FakeMap<K, V> m) { if (m == null) { return; } for (Entry<K, V> entry : m.entrySet()) { this.put(entry.getKey(), entry.getValue()); hashSet.add(hashcode(entry.getKey())); } } @Override public Set<K> keySet() { if (size() == 0) { return null; } Set<K> set = new HashSet<>(); for (Entry<K, V> entry : this.entrySet()) { set.add(entry.getKey()); } return set; } @Override public String toString() { String msg = "FakeHashMap{"; for (Integer h : hashSet) { for (Entry<K, V> entry : list[h]) { msg = msg + "[" + entry.getKey() + "=" + entry.getValue() + "] "; } } msg = msg + "}"; return msg; } }
文件File类
-
文件类对象是java中专门用来操作数据的对象。
- 体现了面向对象的思想
- 通过对象的方法访问和操作文件
- 文件对象是文件对象,文件是文件
- 对象是否创建和文件是否存在没有任何关系
//基于文件的绝对路径 File f1=new File("C:\Users\Administrator\a.txt") //不提供绝对路径,基于项目路径 File f2=new File("text.txt") // 把f1作为父目录创建文件对象 File f3 = new File(f1, "test2.txt");
- 体现了面向对象的思想
-
常用方法
isExists length lastModified setLastModified renameTo isDirectory isFile createFile mkdir mkdirs list listFiles getAbsolutePath getParent getParentFile listRoots delete deleteOnExit//jvm结束删除,常用于临时文件 ...
IO流
-
流:流可以理解为数据传输的通道,介质,媒介
- Java中是基于流直接访问数据,必须基于文件对象访问文件的数据
-
输入输出
-
I/O流
输入流InputStream
输出流OutputStream -
InputStream
read读取数据 close关闭流资源
-
OutputStream
write写入数据 flush缓存提交 close关闭流资源
-
-
流的关闭
- 在try中手动关闭----可能出现异常而不关闭
- 在finally中手动关闭----close也会抛出异常
- 使用try-with-resource----流只要实现了AutoCloseable,流写在with块中,代码结束会自动关闭流
-
流的类型
-
字节流:底层按照字节数据去读取和输入数据,可以支持所有数据的操作;但是字节流读取中文时不是很方便
InputStream和OutputStream 分别对应的是字节输入流和字节输出流 但是本身只是抽象类 只负责定义流需要做的事情 不提供具体的方法实现 主要依靠FileInputStream和FileOutputStream
-
字节输入流FileInputStream
-
由于是读取数据,Java需要准备一个数据容器用来接收从流接收到的文件信息,借助流将数据读取到字节数组中
-
FileInputStream fis =new FileInputStream(f); //创建字节数组,其长度就是文件的长度 byte[] all =new byte[(int) f.length()]; //以字节流的形式读取文件所有内容 fis.read(all); for (byte b : all) { //打印出来是65 66 System.out.println(b); } //每次使用完流,都应该进行关闭 fis.close();
-
-
字节输出流FileOutputStream
-
如果输出流输出的文件不存在,输出流会自动创建该文件,默认情况下,输出流会覆盖文件原有内容;若不希望覆盖,参数后加一个true,则会做拼接而不是覆盖
-
// 准备文件 其中的内容是空的 File f = new File("d:/dsm.txt"); // 准备长度是2的字节数组,用88,89初始化,其对应的字符分别是X,Y byte data[] = { 88, 89 }; // 创建基于文件的输出流 FileOutputStream fos = new FileOutputStream(f); // 把数据写入到输出流 fos.write(data); // 关闭输出流 fos.close();
-
-
-
字符流:是一个经过封装的流,只能用于输入输出文本;对文本进行了封装,无需进行数据处理
-
字符输入流FileReader
-
准备一个字符数据用来存储从流中接收到的字符,通过流将数据读取到数组中
-
因为有中文,通过文件大小计算字符大小,中文占用字节多,导致统值char数组多余的元素会是空字符,即\u0000,需要过滤空字符
-
try (FileReader fr = new FileReader(f)) { // 创建字符数组,其长度就是文件的长度 char[] all = new char[(int) f.length()]; // 以字符流的形式读取文件所有内容 fr.read(all); for (char b : all) { // 打印出来是A B System.out.println(b); } } catch (IOException e) { e.printStackTrace(); }
-
-
字符输出流FileWriter
try (FileWriter fr = new FileWriter(f)) { // 以字符流的形式把数据写入到文件中 String data="abcdefg1234567890"; char[] cs = data.toCharArray(); fr.write(cs); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
-
-
缓存流:字符流的包装流
-
字节流和字符流的弊端:在每一次读写的时候,都会访问硬盘。 如果读写的频率比较高的时候,其性能表现不佳。
-
缓存流:缓存流是高级流,需要基于字符流创建
- 缓存流在读取的时候,会一次性读较多的数据到缓存中,以后每一次的读取,都是在缓存中访问,直到缓存中的数据读取完毕,再到硬盘中读取。
- 缓存流在写入数据的时候,会先把数据写入到缓存区,直到缓存区达到一定的量,才把这些数据,一起写入到硬盘中去。按照这种操作模式,就不会像字节流,字符流那样每写一个字节都访问硬盘,从而减少了IO操作,提高了性能
-
BufferedReader--------readLine
try ( FileReader fr = new FileReader(f); BufferedReader br = new BufferedReader(fr); ) { while (true) { // 一次读一行 String line = br.readLine(); if (null == line) break; System.out.println(line); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
-
BufferedWriter
-
PrintWriter打印流------println
-
构造函数第二个参数true,自动flush,将缓存中的数据刷入
-
try ( // 创建文件字符流 FileWriter fw = new FileWriter(f); //默认情况下 缓存流是会覆盖文本原有所有数据的 所以需要 //在原文本追加数据的情况下 //需要这样写 //FileWriter fw = new FileWriter(f,true); // 缓存流必须建立在一个存在的流的基础上 //这里添加true参数可以自动提交缓存 不需要手动提交 PrintWriter pw = new PrintWriter(fw); ) { pw.println("你爱我呀我爱你"); // 如果需要立刻提交缓存中的数据 可以使用 // pw.flush()进行缓存提交 pw.println("蜜雪冰城甜蜜蜜"); pw.println("喂!三點幾了喂。做做做做撚啊做。飲茶先啊"); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
-
-
-
数据流:是字节流的包装流[使用数据流的writeUTF()和readUTF() 可以进行数据的格式化顺序读写]
-
数据流用于一些需要进行标准格式化输入输出的场景下,进行指定数据类型进行数据传递的场景下,后续会在socket套接字场景下使用到[存储数据时设置数据的数据类型,读取数据的时候必须按照指定数据类型读取]
-
注:数据输出流生成的数据必须由数据读取流读取,否则会产生EOFEexception.因为DataOutputStream 在写出的时候会做一些特殊标记,只有DataInputStream 才能成功的读取。
-
EOFException表示输入过程中意外地到达文件尾或流尾的信号,导致异常。
-
DataInputStream:使用指定类型的方法读取
-
DataOutputStream
-
使用
public static void main(String[] args) { write(); read(); } private static void read() { File f =new File("d:/abaaba.txt"); try ( FileInputStream fis = new FileInputStream(f); DataInputStream dis =new DataInputStream(fis); ){ boolean b= dis.readBoolean(); int i = dis.readInt(); String str = dis.readUTF(); System.out.println("读取到布尔值:"+b); System.out.println("读取到整数:"+i); System.out.println("读取到字符串:"+str); } catch (IOException e) { e.printStackTrace(); } } private static void write() { File f =new File("d:/abaaba.txt"); try ( FileOutputStream fos = new FileOutputStream(f); DataOutputStream dos =new DataOutputStream(fos); ){ dos.writeBoolean(true); dos.writeInt(300); dos.writeUTF("两只老虎爱跳舞"); } catch (IOException e) { e.printStackTrace(); } }
-
-
对象流:对象流也是基于字节流实现的
-
对象流:将java对象转换为特殊字符串或字节码进行存储或传输
- 序列化------将java对象转换为特殊字节码(字符串)的过程
- 反序列化----将特殊字节码(字符串)还原成java对象的过程
-
某个类如果想支持序列化,必须实现指定的序列化接口serializable,要实现序列化的接口必须有一个最终静态常量serialVersionUID
-
序列化在前后端交互的步骤中用的很多 比如把本地java对象或者java集合 转换成JSON字符串或者是JS数组对象格式的字符串
-
常用方法:writeObject,readObject
public class Student implements Serializable { //表示这个类当前的版本,如果有了变化,比如新设计了属性,就应该修改这个版本号 private static final long serialVersionUID = 1L; public String name; public int age; public static void main(String[] args) { //创建一个 Student //要把Student对象直接保存在文件上,务必让Student类实现Serializable接口 Student s = new Student(); s.name = "xdx"; s.age = 666; //准备一个文件用于保存该对象 File f =new File("d:/xdx.txt"); try( //创建对象输出流 FileOutputStream fos = new FileOutputStream(f); ObjectOutputStream oos =new ObjectOutputStream(fos); //创建对象输入流 FileInputStream fis = new FileInputStream(f); ObjectInputStream ois =new ObjectInputStream(fis); ) { oos.writeObject(s); Student s2 = (Student) ois.readObject(); System.out.println(s2.name); System.out.println(s2.age); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
-
-
URL
-
http连接
-
不支持try-with-resource
-
URL:统一资源定位符,可以看作是域名
- 协议+域名+URI(匹配路径+请求参数)
-
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; public class UrlTest { public static void main(String[] args) { //提升作用域 HttpURLConnection connection = null; InputStream is = null; FileOutputStream fos = null; try { URL url = new URL ("任意文件下载地址"); //获取到HttpURLConnction对象 connection = (HttpURLConnection) url.openConnection(); //通过连接对象获取到输入流 is = connection.getInputStream(); //文件保存位置 fos = new FileOutputStream(new File("/file/文件名称")); byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } System.out.println("下载完毕"); } catch (Exception e) { e.printStackTrace(); } finally { //关闭资源 try { fos.close(); is.close(); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } }
-
-
JDBC
-
JDBC:Java Database Connection
-
常用类
-
Connection
-
Statement
// String sql = "update category set name=('" + nname + "') where id=" + id + ""; String sql = String.format("update category set name= '%s' where id= %d", nname, id); try (Connection c = DBUtil.getConnection(); Statement s = c.createStatement()) { s.execute(sql); } catch (SQLException e) { e.printStackTrace(); }
-
PreparedStatement
- 在模糊查询和分页查询中;使用预编译语句,替换Statement
- 预编译,需要在创建preparedStatment时提供sql语句,方便进行编译
- 预编译的情况下,一个?代表的是一个待传参数
- 将sql中的key替换为key的值
- Statement.RETURN_GENERATED_KEYS可以设置自增主键
- 使用预编译传参,参数本身的单引号会由预编译语句自己自行提供,但是我们自己提供的字符串还是需要单引号进行修饰
-
Driver
-
ResultSet:结果集
- 结果集的遍历与集合迭代器的遍历一致
- 每次循环,是从查询结果的一行读取数据
- 读取数据的时候,是按照字段的数据类型进行读取
- 遍历结果的时候使用什么类型的get方法取决于字段的数据类型,可以使用数据的名称作为参数,也可以使用数字的下标作为参数,表明是字段的第几个,1,2,3,
- java中所有增删改excute执行,而查询语句需要使用excuteQuery方法执行,且需要准备一个ResultSet结果集类型的变量去接收从数据库查询的结果
- 结果集的遍历与集合迭代器的遍历一致
-
-
接口化设计
jdk开发组只需要定义jdbc所应该实现的方法和接口,实现类由数据库开发者自行实现,并且统一了jdbc的方法调用规范 数据开发者为jdbc编写的代码最终以jar包的方式出现 所有字符类型的值,需要用单引号进行传值,数字类型的值,不需要单引号
-
JDBC流程
-
驱动加载
Class.forName("com.mysql.jdbc.Driver");
-
创建连接(数据库url,用户名和密码)
Connection connection = DriverManager.getConnection("",'","")
-
提供sql语句
String sql = "insert into category(name) values ('可乐')";
-
创建编译对象,借助编译对象执行sql
Statement s = connection.createStatement(); s.execute(sql);
-
-
模糊查询–PreparedStatement
-
%和_
-
%表示匹配0或1或多个
-
_表示匹配单个
-
-
Statement是先传参,再编译—性能差
-
PreparedStatement是先编译,再传参—性能较好
-
需要用Concat函数拼接字符串
-
Statement和PreparedStatement的区别
- PreparedStatement只需要一次编译,性能较好
- PreparedStatement避免了字符串拼接,使用更方便
- PreparedStatement可以有效避免SQL注入攻击的问题
-
补充
用java PreparedStatement就不用担心sql注入了吗? 1. 简单的在参数后边加一个单引号,就可以快速判断是否可以进行SQL注入,这个百试百灵,如果有漏洞的话,一般会报错。 2.之所以PreparedStatement能防止注入,是因为它把单引号转义了,变成了\',这样一来,就无法截断SQL语句,进而无法拼接SQL语句,基本上没有办法注入了。 3.输入了一个百分号,发现PreparedStatement竟然没有转义,百分号恰好是like查询的通配符。 String sql = "select * from goods where min_name = ?"; // 含有参数 PreparedStatement st = conn.prepareStatement(sql); st.setString(1, "儿童%"); // 参数赋值 System.out.println(st.toString());//com.mysql.jdbc.JDBC4PreparedStatement@8543aa: select * from goods where min_name = '儿童%' 4.此种SQL注入危害不大,但这种查询会耗尽系统资源,从而演化成拒绝服务攻击。 5.解决方案 a.直接拼接SQL语句,然后自己实现所有的转义操作。这种方法比较麻烦,而且很可能没有 PreparedStatement做的好,造成其他更大的漏洞,不推荐。 b.直接简单暴力的过滤掉%
-
public static void testPreparedStatement(String key) { // String sql = "update category set name=('" + nname + "') where id=" + id + ""; String sql = "select * from category where name like concat('%',?,'%')"; try (Connection c = DBUtil.getConnection(); PreparedStatement p = c.prepareStatement(sql)) { p.setString(1, key); ResultSet rs = p.executeQuery(); while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); System.out.println(id + " " + name); } } catch (SQLException e) { e.printStackTrace(); } }
-
补充
sql注入: ' ' or 1=1 其实PreparedStatement并不是真正的避免了SQL注入
-
-
分页查询–PreparedStatement
-
结果集是由预编译对象创建,预编译对象关闭,结果集就会自动关闭
-
public static void testSelectPage(int start, int count) { String sql = "select * from category limit ?,?"; try (Connection c = DBUtil.getConnection(); PreparedStatement p = c.prepareStatement(sql)) { p.setInt(1, start); p.setInt(2, count); ResultSet rs = p.executeQuery(); while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); System.out.println(id + " " + name); } } catch (SQLException e) { e.printStackTrace(); } }
-
-
ORM
- ORM—Object Relation Model—对象映射模型
- 表名映射类名,字段映射成员变量,一行数据映射一个java类对象,字段的值映射java对象的成员变量值,多个数据映射一个java对象集合
-
DAO接口
- 所有对数据库进行操作的方法都应该放在Dao接口的实现类中
多线程
-
概念
- 冯诺依曼体系结构=运算器+控制器+存储器+输入设备+输出设备
- 最早的计算机----批处理系统
- 所有的应用程序依次排队运行
- 逐渐被替换为多程序系统
- CPU通过寄存器记录所打开的文件句柄,程序段和数据段的地址(CPU切换应用程序—上下文切换)
- 进程控制块PCB
- 自动保存的本质:磁盘的IO读写
- 解决方案1:两个进程,一个进程负责与用户交互,一个进程负责IO读写
- 问题:理论上可行,但是由于进程信息的独立性,数据无法直接互通,而进程与进程之间的数据通信的开销过于庞大
- 解决方案2:多进程思想,将一个进程当作一个资源的容器,在其中运行几个轻量级的进程(线程),这些线程共享进程的所有资源,包括但不限于内存地址,全局变量和文件资源
- 每一个线程也有独特的部分,需要记住自己运行到哪一行指令,以及函数的堆栈调用
- 这么设计的目的是为了像切换进程一样切换线程
- IDEA:一个进程负责保存代码数据,进程中有两个线程,一个专门负责和用户交互,另一个专门负责做自动保存
- 解决方案1:两个进程,一个进程负责与用户交互,一个进程负责IO读写
- 进程
- 线程
- CPU
-
Java多线程
- Java中提供了线程相关的对象用来满足程序员的开发和使用场景
-
java中main方法对应的是主线程
-
每个线程的实现类,必须重写父类的run方法
- run方法中的代码代表线程在运行期间内要执行的操作
-
run方法中的检查时异常必须通过try-catch处理
-
- Java中提供了线程相关的对象用来满足程序员的开发和使用场景
-
@FunctionalInterface
- 函数式接口:有且仅有一个抽象方法
- JDK1.8新特性
-
实现线程的方式
-
继承Thread类
-
线程想要被cpu执行,必须通过start方法进入就绪态,只有就绪态的线程才会有被cpu执行的可能,但cpu先调用哪一个线程是完全随机的,线程的切换以及运行时间都是随机的
-
public class FightRice extends Thread { public FightRice(String name) { super(name); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("aaaa" + this.getName()); } } } // FightRice aaa = new FightRice("aaa1"); // FightRice aab = new FightRice("bbb2"); // FightRice aac = new FightRice("ccc3"); // FightRice aad = new FightRice("ddd4"); // aaa.start(); // aab.start(); // aac.start(); // aad.start();
-
-
实现Runnable接口
-
Runnable接口方式的线程无法直接实例化,必须借助Thread类才能完成实例化
-
第三种方式.匿名内部类方式,本质和第一种方式相同
-
public class Student implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("aaaa" + Thread.currentThread().getName()); } } } Thread t1 = new Thread(new Student()); t1.setName("1"); Thread t2 = new Thread(new Student()); t2.setName("2"); t1.start(); t2.start(); Thread t3 = new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("aaaa" + Thread.currentThread().getName()); } } }; t3.setName("abc"); t3.start();
-
-
实现Collable接口
-
-
内部类
- 内部类:名称由JVM顺序分配,但是作为临时类只是用一次
- 类型
- 普通内部类
- 匿名内部类
- 静态内部类
- 成员内部类
-
常用方法
- sleep
- 会使当前的线程暂停运行(休眠)一段时间并且立即放弃对jvm调度资源的竞争
- join
- mian方法本身对应主线程,只有主线程运行了,其他线程对应的对象才会被实例化以及进入到就绪态
- join是插队
- 只有插队的线程运行完才会继续运行main方法(主线程)
- yeild
- 线程让步
- 立刻将jvm调度资源让出,让其他线程有更多的机会去竞争jvm资源,但是让步的线程仍然有竞争的资格
- 考虑优先级
- 线程让步
- setPriority
- 线程的优先级高的资源会有更大的机会被jvm调度,最高为10,最低为1;默认为5;高优先级只意味着机会,不代表绝对,线程的优先级设置必须在进入到就绪态之前
- setDaemon:守护线程
- 会在其他非守护线程运行结束之后才会停止运行
- GC本质也是一个守护线程:程序员无法控制什么时候GC
- sleep
-
线程同步问题
-
原因:代码不存在原子性
-
synchronized关键字
- 线程同步关键字
- 被线程所竞争资源的对象叫做同步对象
- java中规定,每一个同步对象都拥有一个同步对象锁,如果被sychronized所修饰的方法被线程调用执行,第一个进入方法的线程就会获取该同步对象的锁,在该线程持有锁的期间内,其他线程哪怕获取到了jvm的调度资源也无法执行同步方法,其他线程此时就被阻塞
- 被sychronized修饰的方法具有排他性
-
synchronized用法
-
同步方法
public synchronized Ticket getTicket() { if (tickets.size() == 0) { return null; } Ticket ticket = tickets.removeLast(); return ticket; }
-
同步代码块
synchronized (this) { Ticket ticket = tickets.removeLast(); return ticket; }
-
-
死锁
-
一个同步对象只有一个同步对象锁,但一个线程可以持有多个不同的同步对象锁
-
死锁的条件
public class SynTest { public static void main(String[] args) { final Object o = new Object(); final Object o2 = new Object(); Thread t1 = new Thread() { @Override public void run() { System.out.println("t1开始运行"); System.out.println("t1试图获取object1锁"); synchronized (o) { System.out.println("t1拿到o1锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1试图获取object2锁"); synchronized (o2) { System.out.println("t1拿到o2锁"); System.out.println("t1,即将释放o2锁"); } System.out.println("t1,即将释放o1锁"); } System.out.println("t1,运行结束"); } }; Thread t2 = new Thread() { @Override public void run() { System.out.println("t2开始运行"); System.out.println("t2试图获取object2锁"); synchronized (o2) { System.out.println("t2拿到o2锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1试图获取object1锁"); synchronized (o) { System.out.println("t2拿到o1锁"); System.out.println("t2,即将释放o1锁"); } System.out.println("t2,即将释放o2锁"); } System.out.println("t2,运行结束"); } }; t1.start(); t2.start(); } }
-
死锁的必要条件
互斥条件:一个资源每次只能被一个进程使用。 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
-
同步对象的三个方法(用于线程调读:这三个方法是通过同步对象进行调用-----Object类中)
-
wait:让占有当前同步对象的锁的线程进行等待,会临时释放锁
-
wait的使用条件
wait必须出现在synchronized代码块中,处于wait状态的线程如果没有被调用notify方法唤醒,则会一直处于等待状态
-
-
notify
-
每一个同步对象都有一个等待列表,每一个被同步对象wait的线程都会记录在等待列表中,notify方法是从等待列表中选择一个线程进行唤醒,被唤醒的线程会接着之前被wait的地方将没跑完的代码跑完
-
notify是随机唤醒的吗? JDK源码中说明,notify 选择唤醒的线程是任意的,但具体的实现还要依赖于 JVM。也就是说 notify 的唤醒规则,最终取决于 JVM 厂商,不同的厂商的实现可能是不同的,比如阿里的 JVM 和 Oracle 的 JVM,关于 notify 的唤醒规则可能是不一样的。
-
-
notifyAll
- notifyAll是唤醒当前同步对象等待列表中的所有线程
-
-
-
生产者-消费者模型
-
多线程实现的设计模式
-
若干消费者线程:负责处理用户请求
-
若干生产者线程:负责提交用户请求
-
-
代码
public class SteamedBread { private int id; public SteamedBread(int id) { this.id = id; } public SteamedBread() { } @Override public String toString() { return "SteamedBread{" + "id=" + id + '}'; } } public class Basket { int index = 1; SteamedBread[] arrMT = new SteamedBread[6]; //装馒头 public synchronized void push(SteamedBread sb) { while (index == arrMT.length) { System.out.println("篮子满了"); try { Thread.sleep(2000); this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } this.notify(); arrMT[index] = sb; index++; } //拿馒头 public synchronized SteamedBread pop() { while (index == 0) { System.out.println("馒头没了"); try { Thread.sleep(2000); this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } this.notify(); index--; return arrMT[index]; } } public class Producer implements Runnable { Basket bt = null; public Producer(Basket bt) { this.bt = bt; } @Override public void run() { for (int i = 0; i < 20; i++) { SteamedBread steamedBread = new SteamedBread(i); bt.push(steamedBread); System.out.println("生产者生产了馒头" + steamedBread); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Consumer implements Runnable { Basket bt = null; public Consumer(Basket bt) { this.bt = bt; } @Override public void run() { for (int i = 0; i < 20; i++) { for (int j = 0; j < 20; j++) { SteamedBread sb = bt.pop(); if (sb == null) { System.out.println("消费者没取到馒头"); } else { System.out.println("消费者消费了" + sb); } } } } }
-
-
线程池
-
逻辑上与生产者-消费者模型类似
- 定义一个任务容器
- 一次性启动10个消费者线程
- 一开始任务容器为空,所有线程都wait
- 直到有一个外部线程向容器中扔了一个任务,就会有一个消费者线程被唤醒,这个消费者会取出任务,并且执行任务
- 执行完成之后继续等待,直到下一次任务的到来
- 整个过程中不需要创建新的线程,而是循环使用已经存在的线程
-
自定义线程池
public class ThreadPool { //定义线程池大小 int threadPoolSize; //定义存放任务的容器,任务本身应该也是线程 LinkedList<Runnable> tasks = new LinkedList<>(); public ThreadPool() { //容量 this.threadPoolSize = 10; //放入定义的内部类消费者线程 synchronized (tasks) { for (int i = 0; i < threadPoolSize; i++) { new TaskConsumeThread("工具人线程负责消费任务" + i).start(); } } } public void add(Runnable r) { synchronized (tasks) { //我们在初始化线程时会有10个默认的工具人线程进行等待 //而这些线程在线程池被初始化时被wait,为了保证一会儿添加任务后工具人能消费任务,需要唤醒所有处于wait的消费者线程 tasks.add(r); tasks.notifyAll(); } } //定义内部类 内部类是为了方便传参 class TaskConsumeThread extends Thread { //提供构造方法,给消费者线程提供名称 public TaskConsumeThread(String name) { super(name); } //定义要执行的任务 Runnable task; @Override public void run() { while (true) { //在操作访问任务容器的时候,避免某一个消费者线程从任务容器获取任务的时候 // 其他线程也同时来获取任务,确保数据安全 synchronized (tasks) { //如果发现容器为空,所有线程wait while (tasks.isEmpty()) { try { tasks.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } //任务容器不为空时,负责消费任务的消费者线程就可以干活了 //从任务中取出一个task,并且存放在成员变量task中 task = tasks.removeLast(); //允许添加任务的线程继续添加任务 tasks.notifyAll(); } System.out.println(this.getName() + "成功获取到任务并执行"); //执行任务,通过直接调用任务线程的run来模拟任务的执行,通过这种方式调用的run不再具有线程的特性 task.run(); } } } } public class Test { public static void main(String[] args) { //先实例化一个线程池对象,10个消费者线程初始化就已经开始允许 ThreadPool pool = new ThreadPool(); //定义若干个任务线程,定义任务线程需要执行的任务,并且将这些任务线程添加到线程的任务容器中,交给消费者执行就行 for (int i = 0; i < 30; i++) { Runnable task = new Runnable() { @Override public void run() { System.out.println("我是需要要执行的任务线程,我被执行了"); } }; pool.add(task); } //延长主线程的运行时间,给线程池中的线程以及其他线程足够的时间去运行完代码 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
-
Java自带的线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 25, 60, TimeUnit.MICROSECONDS,new LinkedBlockingDeque<>()); //ThreadPoolExecutor(int corePoolSize,//核心线程数 // int maximumPoolSize,//最大线程数 // long keepAliveTime,//存活时间 // TimeUnit unit,//时间单位 // BlockingQueue<Runnable> workQueue)//工作队列 Runnable r = new Runnable() { @Override public void run() { System.out.println("任务1"); } }; threadPool.execute(r);
-
数据库连接池
-
一般的数据库连接的问题
1.每有一个用户访问网站,给该用户分配一个线程,如果用户访问了数据,意味着后台必须给该用户分配一个Connection连接 2.为了保证性能的节约,每个用户在使用完连接后,就会对连接进行关闭,创建和关闭连接是非常消耗性能的,在并发场景下,整个项目的服务器压力会很大,并且一个数据库同时支持的连接总数是有上限的,如果并发量很大,数据库连接的总数就会被消耗光没后续发起的数据库连接会失败
-
数据库连接池
1.连接池在使用前就会创建一定数量的连接,如果有任何线程需要使用连接,就会从连接池中借用而不是重新创建,使用完毕之后,又会吧这个连接归还给连接池以供下一次或者其他线程使用. 2.如果连接池的连接被借用光了,其他线程就会临时等待直到有连接被归还,再继续使用 3.在整个过程中,这些连接不会被关闭,而是不断被循环使用,从而节约了启动和关闭连接的时间
-
德鲁伊连接池(阿里):druid
-
代码
public class ConnectionPool { //准备一个容器用来存放连接 List<Connection> cs = new ArrayList<>(); //定义变量指定连接池大小 int size; //定义初始化方法,用来创建连接对象 private void init() { try { Class.forName("com.mysql.jdbc.Driver"); for (int i = 0; i < size; i++) { Connection c = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf-8", "root", "123456"); cs.add(c); } } catch (Exception e) { e.printStackTrace(); } } //定义构造方法,用于初始化size以及调用init方法创建链接 public ConnectionPool(int size) { this.size = size; init(); } public synchronized Connection getConnection() { while (cs.isEmpty()) { try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } return cs.remove(0); } public synchronized void returnConnection(Connection c) { cs.add(c); this.notifyAll(); } } public class WorkingThread extends Thread { private ConnectionPool cp; public WorkingThread(String name, ConnectionPool cp) { super(name); this.cp = cp; } @Override public void run() { //先借用连接 Connection c = cp.getConnection(); System.out.println(this.getName() + "获取了一个连接,并开始工作"); try { PreparedStatement ps = c.prepareStatement("select * from student"); ps.executeQuery(); //模拟sql语句执行所耗费的时间 Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } cp.returnConnection(c); } } public class Test { public static void main(String[] args) { //实例化连接池对象 ConnectionPool cp = new ConnectionPool(3); for (int i = 0; i < 100; i++) { new WorkingThread("工作线程" + i, cp).start(); } } }
-
-
-
锁的类型
-
悲观锁
-
syschronized:代码运行结束自动释放锁
-
RetrantLock
-
可以有效地避免死锁
-
通过Lock或tryLock获取锁,无法自动释放锁,需要通过unlock方法手动释放锁
-
使用
public class Test { public static String now() { return new SimpleDateFormat("hh:mm:ss").format(new Date()); } public static void log(String msg) { System.out.printf("%s %s %s %n", now(), Thread.currentThread().getName(), msg); } public static void main(String[] args) { Lock lock = new ReentrantLock(); Thread t1 = new Thread() { @Override public void run() { try { log("线程启动"); log("试图获取锁"); lock.lock(); log("成功获取锁"); log("即将进行五秒业务操作,这里用Sleep模拟"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } finally { log("释放锁"); lock.unlock(); } log("线程结束"); } }; t1.setName("线程1"); t1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } Thread t2 = new Thread() { @Override public void run() { try { log("线程启动"); log("试图获取锁"); lock.lock(); log("成功获取锁"); log("即将进行五秒业务操作,这里用Sleep模拟"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } finally { log("释放锁"); lock.unlock(); } log("线程结束"); } }; t2.setName("线程2"); t2.start(); } }
-
-
线程的交互(await,signal,signalAll)
public class Test2 { public static String now() { return new SimpleDateFormat("hh:mm:ss").format(new Date()); } public static void log(String msg) { System.out.printf("%s %s %s %n", now(), Thread.currentThread().getName(), msg); } public static void main(String[] args) { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Thread t1 = new Thread() { @Override public void run() { try { log("线程启动"); log("试图获取锁"); lock.lock(); log("成功获取锁"); log("进行五秒数据操作,Sleep模拟"); Thread.sleep(5000); log("临时释放锁,进入等待状态"); condition.await(); log("重新获取锁,并执行,Sleep模拟"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } finally { log("释放锁"); lock.unlock(); } log("线程结束"); } }; t1.setName("t1"); t1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } Thread t2 = new Thread() { @Override public void run() { try { log("线程启动"); log("试图获取锁"); lock.lock(); log("成功获取锁"); log("进行五秒数据操作,Sleep模拟"); Thread.sleep(5000); log("唤醒等待中的t1线程"); condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { log("释放锁"); lock.unlock(); } log("线程结束"); } }; t2.setName("t2"); t2.start(); } }
-
tryLock()方法:会尝试一秒内获取锁,获取到了返回true,否则返回false并且放弃获取锁
public class Test3 { public static String now() { return new SimpleDateFormat("hh:mm:ss").format(new Date()); } public static void log(String msg) { System.out.printf("%s %s %s %n", now(), Thread.currentThread().getName(), msg); } public static void main(String[] args) { Lock lock = new ReentrantLock(); Thread t1 = new Thread() { @Override public void run() { boolean locked = false; log("线程开始"); log("试图获取锁"); //会尝试一秒内获取锁,获取到了返回true,否则返回false并且放弃获取锁 try { locked = lock.tryLock(); if (locked) { log("成功获取锁"); log("sleep模拟5s"); Thread.sleep(5000); } else { log("经过尝试获取锁1s,但是没有获取到"); } } catch (Exception e) { e.printStackTrace(); } finally { if (locked) { log("释放锁"); lock.unlock(); } } } }; t1.setName("t1"); t1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } Thread t2 = new Thread() { @Override public void run() { boolean locked = false; log("线程开始"); log("试图获取锁"); //会尝试一秒内获取锁,获取到了返回true,否则返回false并且放弃获取锁 try { locked = lock.tryLock(); if (locked) { log("成功获取锁"); log("sleep模拟5s"); Thread.sleep(5000); } else { log("经过尝试获取锁1s,但是没有获取到"); } } catch (Exception e) { e.printStackTrace(); } finally { if (locked) { log("释放锁"); lock.unlock(); } } } }; t2.setName("t2"); t2.start(); } }
-
补充:Lock和synchronized
sychronized和Lock的区别 1.Lock是一个接口,而sychronized是java的关键字 2.sychronized是内置的语言实现,Lock是通过代码实现 3.Lock可以选择性地获取锁,如果一段时间获取不到,可以放弃;sychronized无法放弃获取锁 4.可以利用Lock的这个特性,有效地避免死锁,synchronized必须通过谨慎和良好的设计才能减少死锁的发生 Lock和synchronized共同点 1.本质都是悲观锁,消耗的性能较大,会影响代码效率
-
原子操作
- jdk6之后,新增了一个包,这个包中的所有类都是线程安全的(java.util.concurrent)
-
-
乐观锁
- volatile:保证数据的可见性
-
-
Java内存模型
-
直接内存:用于java的NIO(非阻塞通信)使用的,用于字节缓存数据的直接通信
socket通信: bio 阻塞通信 nio 非阻塞通信 aio
-
堆:用于存储对象
-
方法区:存放的是每一个类的信息(包括了类名称,方法信息,字段信息,静态变量,常量,以及编译器编译之后的代码等,这些数据都是存放在class文件中),
- class文件中除了类的字段,方法,接口等描述信息外,还有常量池用来存储编译期间生成的字面量和引用
- 方法区中最重要的一个部分是运行时常量池
-
运行时常量池:存放运行时的常量(部分通过等号赋值的String字符串,以及封装类的部分数据)
- 每个类或接口中的常量池的运行时表现形式在类和接口被加载到jvm之后的运行时常量池就被创建出来
-
程序计数器
- 计算机中:保存程序当前执行的指令的地址(下一条指令存储单元的地址)
- jvm中:
- 多线程是通过线程轮流切换获取cpu执行,在任意时刻内,一个cpu内核只会执行一条线程中的指令
- 为了能够使每一个线程在线程切换后都能够恢复切换之前的程序执行位置,每一个线程都需要有自己独立的程序计数器且不相互干扰
-
本地方法栈:和虚拟机栈几乎一致,区别在于所服务的方法(只服务于native方法)
-
虚拟机栈
- Java栈中存放的是一个一个的栈帧,每一个栈帧对应一个被调用的方法,帧中包括局部变量表、指向当前方法的所属类的运行时常量池的引用、方法返回的地址和一些其他的信息
- 当线程执行一个方法,就随之创建一个对应的栈帧,并且将建立的栈帧压入栈中,方法执行完毕后,会将栈帧弹栈,意味着线程当前执行的方法所对应的栈帧一定位于java栈的顶部
- 当方法被调用的时候,方法压栈;当方法调用了其他方法,则继续将新方法压栈,当方法全部运行完的时候,出栈
- 补充
- 局部变量表:存储方法中的局部变量,包括方法的形参以及方法中声明的非静态变量.对于基本数据类型的变量,直接存储其值,对于引用类型的变量,存储其对应对象的内存地址
- 操作数栈:程序中的所有计算过程都是借助操作数栈所实现的
- 指向运行时常量池的引用:方法在执行的过程中可能用到类中的常量,所以必须有一个引用指向运行时常量
- 方法返回地址:当一个方法执行完毕之后要返回之前调用它的地方,所以栈中要保存一个方法返回地址
- 每个线程所执行的方法可能是不同的;每一个线程应该拥有自己私有的栈内存区域和对应的方法调用的栈帧
-
-
volatile乐观锁
volatile的用法
public class Test4 extends Thread { private volatile boolean flag = false; public boolean isFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } flag = true; System.out.println("flag:" + flag); } } class TestVolatile { public static void main(String[] args) { Test4 t = new Test4(); t.start(); while (true) { if (t.isFlag()) { System.out.println("你干嘛"); } } } }
volatile提供了变量的可见性
JMM----Java内存模型
-
为了屏蔽底层不同的计算机的区别
-
计算机模型
-
早期计算机模型,cpu和内存的计算速度几乎是相同的
-
现代计算机,cpu和内存之间添加了高速缓存cache,解决了cpu和内存之间的速度差异问题
-
缓存一致性问题
- 每一个处理器都有自己对应的高速缓存,而所有cpu共享一个主内存
- 缓存一致性协议
-
-
JMM(Java Memory Model)有如下规定
- 所有共享变量都存储于主内存(成员变量和类变量),这里涉及的变量不包含局部变量,因为局部变量是线程私有的
- 每一个线程有一个自己的工作内存,保留了被线程使用的变量的工作副本,线程对变量的所有操作(读写)都必须在工作内存中完成,不可以直接对主内存进行读写
-
volatile实现的结果也可以通过加锁的方式实现
- 当某一个线程进入sychronized代码块后,线程会获得锁,线程获得锁的同时会立即先清空自己的工作内存,再从主内存拷贝共享变量最新的值到工作内存中成为副本
- 如果sychronized代码块中对变量的值进行修改,将修改后的值刷新到主内存中,然后再释放
-
每一个线程在操作数据的时候会把是数据从主内存读取到自己的工作内存中,同时如果该线程操作了数据,并且将其写回到主内存中,其他已经读取了副本的线程对应的该变量就失效了;所以线程需要重新从主内存进行读取,volatile保证不同程序对共享变量操作的可见性
- 一个线程修改了volatile的变量,当修改协会到主内存的时候,另一个线程会立刻看到最新的值
-
缓存一致性协议
- 最经典的MESI(intel)
- cpu写数据时,如果发现操作的数据是共享变量;也就是其他cpu中也存在该变量的副本,就会发出信号将其他cpu该变量的缓存行设置为无效状态
- 这个时候,其他cpu需要读取这个变量的时候,发现这个变量的缓存行已经被标记为失效状态,则会从内存中重新读取
- 最经典的MESI(intel)
-
如何发现数据失效?
-
CPU通过嗅探去判断数据失效,每一个CPU通过嗅探在总线上传播的数据,来检查自己缓存的数据是否过期
-
可能带来的问题?
总线风暴 volatile底层是通过CAS算法实现的 volatiel使用MESI缓存一致性协议,需要不断地从主内存嗅探和CAS循环,会产生大量的无效交互,会导致总线的带宽达到峰值 所以不要在项目的所有地方使用volatile
-
-
volatile可以禁止指令重排序
-
指令重排序的情况
1.编译器优化的重排序。 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 2.指令级并行的重排序。 现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 3.内存系统的重排序。 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 as-if-serial和happens-before的主要作用都是:在保证不改变程序运行结果的前提下,允许部分指令的重排序,最大限度的提升程序执行的效率。
-
as-if-serial语义
不管指令怎么重排序,在单线程下执行结果不能被改变。不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。 //数据依赖 int a = 1; // 1 int b = 2; // 2 int c = a + b; // 3 上述代码,a和b不存在依赖关系,所以1、2可以进行重排序;c依赖 a和b,所以3必须在1、2的后面执行。 //控制依赖 public void use(boolean flag, int a, int b) { if (flag) { // 1 int i = a * b; // 2 } } flag和i存在控制依赖关系。当指令重排序后,2这一步会将结果值写入重排序缓冲(Reorder Buffer,ROB)的硬件缓存中,当判断为true时,再把结果值写入变量i中。
-
happens before语义
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 规则: 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。 主要含义是:在一个线程内不管指令怎么重排序,程序运行的结果都不会发生改变。和as-if-serial 比较像。 监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。 主要含义是:同一个锁的解锁一定发生在加锁之后 管程锁定规则: 一个线程获取到锁后,它能看到前一个获取到锁的线程所有的操作结果。 主要含义是:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现) volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 主要含义是:如果一个线程先去写一个volatile变量,然后另一个线程又去读这个变量,那么这个写操作的结果一定对读的这个线程可见。 传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C。 start()规则: 如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 主要含义是:线程A在启动子线程B之前对共享变量的修改结果对线程B可见。 join()规则: 如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 主要含义是:如果在线程A执行过程中调用了线程B的join方法,那么当B执行完成后,在线程B中所有操作结果对线程A可见。 线程中断规则: 对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。 主要含义是:响应中断一定发生在发起中断之后。 对象终结规则: 就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
-
内存屏障
-
并发环境下指令重排序带来的问题:这里有两个线程A和线程B,当A执行
init
方法时发生了指令重排,2先执行,这时线程B执行use
方法,这时我们拿到的变量a却还是0,所以最后得到的结果 i=0,而不是i=1。 -
解决方法
内存屏障(volatile) 或 临界区(synchronized)
-
内存屏障
使用内存屏障,那么JMM的处理器,会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。 https://blog.csdn.net/xiaolyuh123/article/details/103289570
-
-
乐观锁和悲观锁的区别以及应用场景
/** * volatile适用于以下场景: * 1.某个属性被多个线程共享 其中一个线程修改了此属性,其他线程可以立刻获取修改之后的值 * 2.volatile的读写操作都是无锁的 无法替代synchronized * volatile并没有提供原子性 和排他性 只是因为无锁 * 不需要花费时间再获取锁和修改锁上 所以volatile的成本较低 * 3.volatile只能用于修饰属性 可以避免指令的重排序 * 4.volatile提供了可见性 并且volatile修饰的属性 * 不会被线程缓存 永远从主内存中读取 * volatile可以是long 和 double 的赋值 是原子性 * 5.volatile可以在单例的双重检查锁模式下 实现 * 可见性和禁止重排序 * 6.volatile可以当作是轻量级的synchronized * volatile本身不保证原子性 但是如果是对一个共享变量进行多个线程的赋值 * 而没有其他的操作 就可以使用volatile来代替synchronized * 因为赋值本身是具有原子性的 无法被线程切换打断 */
-
-
-
CAS算法
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。 CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS算法可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等
-
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
-
循环时间长开销很大
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
-
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
-
ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗? 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
-
-
Socket编程
-
B/S C/S–>服务器端,客户端
- socket客户端
- serverSocket服务器端
-
端口号80可以省略不写(默认端口)
-
代码
/* *服务器端 * */ public class Server { public static void main(String[] args) throws Exception { //创建服务器端,指定服务器端口8888 ServerSocket ss = new ServerSocket(8888); //在8888端口上进行监听,如果没有客户端发送请求,服务器就会一直处于监听状态 Socket s = ss.accept(); System.out.println("接收到了请求" + s); s.close(); ss.close(); } } /* *客户端 * */ public class Client { public static void main(String[] args) throws Exception { //创建本地客户端,并且指定客户端所连接的服务器的 //ip地址和端口,客户端自己的端口号由java自动分配 Socket s = new Socket("127.0.0.1", 8888); System.out.println(s); s.close(); } }
-
数据通信
public class Server { public static void main(String[] args) throws Exception { //创建服务器端,指定服务器端口8888 ServerSocket ss = new ServerSocket(8888); //在8888端口上进行监听,如果没有客户端发送请求,服务器就会一直处于监听状态 Socket s = ss.accept(); //客户端和服务器数据的传输,都依赖于流实现 //根据服务器端接收到的客户端获取对应的输入流 InputStream is = s.getInputStream(); //基于这个输入流,获取对应的数据输入流 DataInputStream dis = new DataInputStream(is); //再根据服务器端接收到的客户端获取对应的输出流 OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); Scanner sc = new Scanner(System.in); while (true) { System.out.println("收到客户端消息为" + dis.readUTF()); dos.writeUTF(sc.nextLine()); } } } public class Client { public static void main(String[] args) throws Exception { //创建本地客户端,并且指定客户端所连接的服务器的 //ip地址和端口,客户端自己的端口号由java自动分配 Socket s = new Socket("127.0.0.1", 8888); OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); Scanner sc = new Scanner(System.in); while (true) { dos.writeUTF(sc.nextLine()); System.out.println("收到服务器消息为:" + dis.readUTF()); } } }
-
实现聊天室
public class Client { public static void main(String[] args) { try { System.out.println("请输入要访问的聊天室ip地址"); Scanner sc = new Scanner(System.in); String ip = sc.nextLine(); //建立客户端socket对象,向服务器发送请求 Socket socket = new Socket(ip,8888); new SendThread(socket).start(); new ReceiveThread(socket).start(); } catch (IOException e) { throw new RuntimeException(e); } } } public class ReceiveThread extends Thread{ Socket socket; public ReceiveThread(Socket socket) { this.socket = socket; } @Override public void run() { try{ while (true){ if (socket.isConnected()){ InputStream is = socket.getInputStream(); DataInputStream dis = new DataInputStream(is); System.out.println(dis.readUTF()); } } }catch (Exception e){ }finally { try{ socket.close(); System.out.println("聊天结束"); }catch (Exception e){ e.printStackTrace(); } } } } public class SendThread extends Thread{ Socket socket; public SendThread(Socket socket) { this.socket = socket; } @Override public void run() { try{ Scanner sc = new Scanner(System.in); while (true){ String str = sc.nextLine(); if (socket.isConnected()){ OutputStream os = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); dos.writeUTF(str); } } }catch (Exception e){ }finally { try{ socket.close(); System.out.println("聊天结束"); }catch (Exception e){ e.printStackTrace(); } } } } public class Server { private static ServerSocket ss; private static volatile List<Socket> list = new ArrayList<>(); public static void main(String[] args) throws IOException { ss = new ServerSocket(8088); while (true) { Socket accept = ss.accept(); list.add(accept); new SeverThread(accept, list).start(); } } } public class SeverThread extends Thread { Socket socket; List<Socket> list; InputStream is; DataInputStream dis; OutputStream os; DataOutputStream dos; public SeverThread(Socket s, List<Socket> list) { this.socket = s; this.list = list; } @Override public void run() { try { while (true) { is = socket.getInputStream(); dis = new DataInputStream(is); String str = dis.readUTF(); System.out.println(socket.getInetAddress() + ":" + str); //遍历所有socket,向用户广播消息 Iterator<Socket> iterator = list.iterator(); while (iterator.hasNext()) { Socket st = iterator.next(); if (st.isConnected()) { os = st.getOutputStream(); dos = new DataOutputStream(os); dos.writeUTF(socket.getInetAddress() + ":" + str); } else { synchronized (list) { iterator.remove(); System.out.println(st.getInetAddress() + "已经退出聊天室,当前在线人数为" + list.size()); } } } } } catch (Exception e) { } finally { try { if (!socket.isConnected()) { socket.close(); list.remove(socket); System.out.println(socket.getInetAddress() + "已经退出聊天室,当前在线人数为" + list.size()); } } catch (IOException e) { e.printStackTrace(); } } } }
-
多线程版本
public class ReceiveThread extends Thread { private Socket s; public ReceiveThread(Socket s) { this.s = s; } @Override public void run() { try { InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); while (true) { System.out.println(dis.readUTF()); } } catch (IOException e) { e.printStackTrace(); } } } public class SendThread extends Thread { private Socket s; public SendThread(Socket s) { this.s = s; } @Override public void run() { try { OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); Scanner sc = new Scanner(System.in); while (true) { dos.writeUTF(sc.nextLine()); } } catch (IOException e) { e.printStackTrace(); } } } public class Server { public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(8080); Socket s = ss.accept(); new SendThread(s).start(); new ReceiveThread(s).start(); } catch (Exception e) { e.printStackTrace(); } } } public class Client { public static void main(String[] args) { try { Socket s = new Socket("127.0.0.1", 8080); new SendThread(s).start(); new ReceiveThread(s).start(); } catch (Exception e) { e.printStackTrace(); } } }
-
项目打包
- Project Structure
- Artifacts
- jar---->from module…
- Artifacts
- build
- build artifacts
- build
- java -jar 路径/文件.jar
- Project Structure
反射
-
反射:将类当作对象进行处理
- 如果将类当作一个对象,类的方法、成员变量、构造方法都作为类对象的属性
- 在获取类对象时,类的静态属性会自动加载
- 反射机制中,万物皆对象,构造方法 方法 成员变量都可以当作对象来进行使用
-
每个类的类对象是唯一的,获取方式如下:
1.通过Class.forName()获取 --加载静态 2.通过.class方式直接获取 --非加载 3.通过类的实例对象获取new Instance().getClass() --加载静态和非静态
-
利用反射机制创建对象
public static void main(String[] args) throws Exception { //1.获取类对象 Class c = Class.forName("全类名"); //2.根据类对象获取构造方法的反射对象 Constructor c1 = c.getConstructor(); //如果要获取有参的反射,需要提供构造器的每个参数的参数类型反射对象 Constructor c2 = c.getConstructor(Integer.class, String.class); //3.通过构造方法的反射对象创建对象 Student s = (Student) c1.newInstance(); //4.访问属性 System.out.println(s); }
-
调用方法和变量
public static void main(String[] args) throws Exception { Class c = Class.forName("全类名"); Student s = (Student) c1.newInstance(); Method m = c.getMethod("setName",String.class); m.invoke(s,"参数"); Field f = c.getFiled("sno"); }
-
反射的作用
-
反射是Spring的底层实现
- IOC控制反转,将对象的创建权力由开发者交由代码自身创建
- DI依赖注入,对象的初始化或者赋值由代码实现
-
代码
public interface UserService { void login(); void register(); } public class UserServiceImpl1 implements UserService { @Override public void login() { System.out.println("账号"); } @Override public void register() { System.out.println("验证码注册"); } } public class UserServiceImpl2 implements UserService { @Override public void login() { System.out.println("扫码"); } @Override public void register() { System.out.println("微信快速注册"); } } public class TestUserService { public static void main(String[] args) throws Exception { File f = new File("D:\\Development Environment\\Project\\IDEA Project\\Airui\\src\\main\\java\\com\\iweb\\study\\day13\\userService.config"); //创建读取配置文件的对象 Properties properties = new Properties(); //加载配置文件信息 properties.load(new FileInputStream(f)); //这个类会将属性和属性名封装为map结构,只需要通过key进行读取,获取属性 String className = (String) properties.get("class"); String methodName = (String) properties.get("method"); //获取类对象 Class clazz = Class.forName(className); //获取构造器对象 Constructor c = clazz.getConstructor(); //获取方法对象 Method m = clazz.getMethod(methodName); //借助构造器,完成对象创建 UserService userService = (UserService) c.newInstance(); //利用method对象完成方法调用 m.invoke(userService); } }
-
-
基于配置文件的思想
- 约定优于配置,配置优于实现
-
补充
public static void main(String[] args) { System.out.println(int.class==Integer.TYPE);//结果为true System.out.println(int.class==Integer.class);//结果为false } int.class表示基本数据类型int的Class对象,TYPE是Integer中的静态常量,api中已经写明它表示的是基本数据类型int的class实例,因此先打印出的是true Integer.class表示引用数据类型Integer的class对象,与前者肯定是不相等的. 注意:由.class来创建Class对象的引用时,不会自动初始化Class对象.
数据结构
-
排序算法
-
冒泡排序
for(int i=0;i<length-;i++){ for(int j=0;j<length-1-i;j++){ if(arr[j]>arr[j+1]){ swap(arr[j],arr[j+1]) } } }
-
选择排序:每一轮将当前元素与所有待排元素比较,小的则交换;每次确定一个最终位置
-
-
数组:数组查找快,在尾部增删快,但在中间或头部增删慢
-
链表:链表查找相对较慢,增删元素较快
- 单向链表
- 双向链表
-
Hash算法/哈希表
-
Hash算法:空间换时间
-
Hashcode:模运算—>hashcode值
-
hash冲突:一个hashCode值匹配多个数据,叫做hash冲突,设计hashcode方法时要减少hash冲突
-
JDK的HashMap
-
JDK1.7之前:Java的HashMap使用的是数组加链表的方式
- 调用key所对应的equals方法判断两个key是否相同,找不到就新建键值对,相同就不新建键值对,而用新的value替换旧的value
- 先计算hashcode判断要存的数组的下标,然后查看该下标下有没有元素,没有直接存储,否则,遍历该数组下标链表元素,调用equals方法判断是否有相同的key,找不到就新建键值对,否则用旧的value替换新的value
-
JDK1.8相对于JDK1.7
- 如果数组长度大于64并且链表长度大于8就自动将链表转换为红黑树
- 退化为链表
- 扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表。
- 移除元素 remove( ) 时,在removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 根的左子树的左子树为空,都会发生红黑树退化成链表。
-
-
补充:
-
两个对象相同,hashcode一定相同;hashcode相同两个对象不一定相同
-
为什么hashSet不允许重复,还不能保证存读顺序一致
- HashSet底层是一个HashMap,只不过hashset使用的是map的key,而所有的value都指向同一个最终静态常量对象object
- Set的值相当于Map的key,key的最终存放位置只和key的hashcode值有关,而遍历的时候,是从底层数组的第一项往后遍历的
-
-
手撕HashMap
-
-
红黑树
-
红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。因此,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。
-
红黑树的特点
1.节点是红色或黑色。 2.根是黑色。 3.所有叶子都是黑色(叶子是NIL节点)。 4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。) 5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(简称黑高)。
-
-
二叉树
二叉树的特点 1.每个节点都可以有左右子节点 2.每个节点都有一个值 3.我们可以借助java类去构建二叉树的模型
-
手撕二叉树
@Data @NoArgsConstructor @AllArgsConstructor //泛型继承 表示 你提供的泛型类型一定是Comparable类的子类 public class Node<T extends Comparable> { private T value; private Node<T> leftNode; private Node<T> rightNode; public void add(T v){ //如果当前节点没有值 则把数据放在当前节点上 if(null == value){ value = v; }else { //如果当前节点有值 则需要进行左子或者右子的递归遍历 //一直到找到某一个空节点 才可以把值存入 //判断新增的值 比当前值小或者相对等的情况下 //应该不断遍历左子节点 直到找到合适的位置存放值 //这里使用compareTo方法进行比较 是为了方便我们后续进行拓展 //如果二叉树节点存放的并不是int值 而是对象 //我们就可以让对象实现Comparable接口 对对象进行排序 if (value.compareTo(v) >= 0) { //当前值在比对之后发现小于节点值 则先判断当前节点的左子节点是否为空 //如果为空 则创建对应的左子节点对象 将值存入 if (null == leftNode) { leftNode = new Node(); } //通过递归调用 直到找到合适的节点存放v这个值 递归结束 leftNode.add(v); } else { //如果新增值比当前的节点值大 if (null == rightNode) { rightNode = new Node(); } rightNode.add(v); } } } public List<T> inorder(){ //递归调用的每一层都有一个集合 用来存储节点数据 List<T> values = new ArrayList<>(); //对左子树进行遍历 //如果当前节点的左子节点不为空 则递归调用 继续往下遍历 if(null != leftNode){ values.addAll(leftNode.inorder()); } // //如果左子不为空 能运行到这一行的时候 说明节点的左子树已经 //遍历完了 则 将节点值添加到values集合中 values.add(value); //对右子树进行判断 if(null!=rightNode){ values.addAll(rightNode.inorder()); } return values; } public static void main(String[] args) { //准备数据 // Integer[] randoms = {60,37,72,78,44,56,70,33}; //创建根节点 // Node root = new Node(); //遍历数组 将数组中的所有数据存放到根节点中 //利用add方法自动创建关联的左子节点或者右子节点 //并且把值放入到节点中 // for (int n:randoms) { // root.add(n); // } // System.out.println(root.rightNode.leftNode.getValue()); // System.out.println(root.leftNode.rightNode.rightNode.getValue()); List<Student> stus = new ArrayList<>(); stus.add(new Student(1,188)); stus.add(new Student(1,170)); stus.add(new Student(1,150)); stus.add(new Student(1,175)); stus.add(new Student(1,163)); //创建根节点 并提供对应泛型 Node<Student> root = new Node(); for (Student s:stus) { root.add(s); } // System.out.println(root.leftNode.leftNode.rightNode.getValue().getHeight()); System.out.println(root.inorder()); } //准备一个10W个随机数的集合 数字范围在1-1000 // List<Integer> randomNumlist = //分别使用二叉树 冒泡排序对这个集合中的数据进行排序 然后比较排序的效率 }
-
二叉树的节点
public class Node { // 左子节点 public Node leftNode; // 右子节点 public Node rightNode; // 值 public Object value; }
-
二叉树排序:小与等于放左边,大于放右边
-
插入
// 插入 数据 public void add(Object v) { // 如果当前节点没有值,就把数据放在当前节点上 if (null == value) value = v; // 如果当前节点有值,就进行判断,新增的值与当前值的大小关系 else { // 新增的值,比当前值小或者相同 if ((Integer) v -((Integer)value) <= 0) { if (null == leftNode) leftNode = new Node(); leftNode.add(v); } // 新增的值,比当前值大 else { if (null == rightNode) rightNode = new Node(); rightNode.add(v); } } }
-
二叉树的遍历
//左序(前序) public List<Object> values() { List<Object> values = new ArrayList<>(); // 左节点的遍历结果 if (null != leftNode) values.addAll(leftNode.values()); // 当前节点 values.add(value); // 右节点的遍历结果 if (null != rightNode) values.addAll(rightNode.values()); return values; } //中序 //右序(后序)
-
-
补充
-
代码分层
- pojo层:存放实体类
- controller层:控制器层,负责接收用户输入的数据,跳转到不同的视图
- service层:业务层,所有的数据操作和业务逻辑都应该放在这个层中
- view层:视图层,负责提供用户能看到的界面
- util层:工具类,如验证码生成等
- dao层
-
MySQL
-
数据库操作
drop database xwh create database xwh default chaset=utf8 use xwh
-
数据类型
常用数据类型 整型 tinyint 小整型 1byte 通常会结合unsigned使用,表示无符号小整型 smallint较小整型 2byte mediumint中整型 4byte bigint大整型 8byte 浮点型 float 4byte double 8byte decimal 字符串类型的浮点数,一般用于金融类计算 字符类型 char固定长度字符串 最大255 varchar可变长字符串 0~65536 tinytxt微型文本 2^8-1 TEXT文本类型 2^18-1 日期类型 DATE YYYY-MM-DD TIME HH:mm:ss DATETIME YYYY-MM-DD HH:mm:ss TIMESTAMP时间戳
-
引擎:mysql的表可以选择不同的mysql引擎,不同的数据库引擎支持的功能不一样
innodb 支持事务管理 安全性高 支持多表多用户操作 myisam 节约空间,速度快
-
数据库操作
-
建表
CREATE TABLE student( id INT(10) NOT NULL AUTO_INCREMENT COMMENT '主键', NAME CHAR(6) NOT NULL DEFAULT '匿名', gender CHAR(2) NOT NULL DEFAULT '女', birthday DATETIME DEFAULT NULL, address VARCHAR(300) DEFAULT NULL, qqnumber INT(11)UNSIGNED DEFAULT NULL, PRIMARY KEY(id) )ENGINE=INNODB DEFAULT CHARSET=utf8;
-
修改数据库
修改表名alter table 旧表名 rename as 新表名 增加字段alter table 表名 add 列名(长度) 修改字段alter table 表名 modify 列名(长度) 删除字段alter table 表名 drop 列名(长度)
-
删除表
drop table 表名 drop table if exsits 表名
-
-
约束:mysql常用约束与oracle一致
-
常用日期函数
now()返回当前时间 对应datatime类型 currdate只返回年月日 currtime只返回时分秒
-
DML语句
-
insert
--mysql中与oracle类似,可以指定部分插入值,也可以全部插入 insert into table(column) values() --mysql支持一条语句插入多条数据 insert into table(column) values() , values() 注意点 1.字段与字段之间由逗号隔开 2.字段可以省略,但是值必须全部提供并且一一对应 3.可以同一时间插入多条数据,values后面需要用,隔开
-
update:语法与oracle完全一致
--如果不添加where条件,表中所有数据都会被修改 update 表名 set ~ where 条件
-
delete
自动删除符合where条件的语句,没有where 自动删除表的所有行 delete from 表名 where 条件
-
-
select语句
select distinct --- from --- where --- group by --- having --- order by ---
-
分页函数(mysql没有rowid和rownum)
select ..... limit 1,2 SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15 SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last. SELECT * FROM table LIMIT 5; //检索前 5 个记录行 ...等同于 0,5
-
模糊查询
select * from ~ where ? like '%w%' select * from ~ where ? like concat('%','w%') ---后续在使用jdbc要使用concat函数进行拼接
-
函数:与oracle一致----sum/avg/max/min/count
-
时间函数:year() month() day()
-
数据库分区
-
range分区:范围分区
create table 表名( ...., )partition by range(字段名) ( partition 分区名1 values less than(6), --1~6的字段值 partition 分区名2 values less than(12), --6~12的字段值 .... );
-
list分区:列表分区,允许用户将不相关的数据组织起来
create table 表名( ...., )partition by list(字段名) ( partition p0 values(值1,值2...) partition p1 values(值4,值9...) .... );
-
hash分区:散列分区,允许用户对不具有逻辑范围的数据进行分区,通过在分区上执行hash函数决定存储的分区,并将数据平均分布到不同分区
create table 表名( ...., )partition by hash(字段名) ( partition p0, partition p1, .... );
-
复合分区:其他分区组合使用
CREATE TABLE RHTABLE ( EMPNO VARCHAR(20) NOT NULL , EMPNAME VARCHAR(20) , DEPTNO INT , BIRTHDATE DATE NOT NULL , SALARY INT ) PARTITION BY RANGE(SALARY) SUBPARTITION BY HASH(YEAR(BIRTHDATE)) SUBPARTITIONS 3 ( PARTITION P1 VALUES LESS THAN(200) , PARTITION P2 VALUES LESS THAN MAXVALUE ) ;
-
-
如何提高数据库查询性能
索引 查询时指定字段不要用* 用分页关键字(mysql limit/oracle rownum) 数据库分区 https://blog.csdn.net/yangbaggio/article/details/105739281 读写分离 MyCat Server
-
存储过程
- create or replace procudure is
- 调用存储过程:call 存储过程名(参数);
-
-
设计模式
-
简述
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。 https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247517079&idx=2&sn=6406008df0b99ee3d97bbceaa278f2f2&chksm=fbb10a69ccc6837fde5639b70b4c8ef4f0902f3b728dd4f9a4bded687cac5ff8998af17e0719&scene=27 https://zhuanlan.zhihu.com/p/575645658
-
单例模式
-
懒汉:延迟加载
public class Singleton { //2.定义一个当前类自己类型的静态引用 private static Singleton s1; private Singleton() { //1.构造方法私有化 } //3.提供一个共有的get方法,用于外界获取对象 public static Singleton getInstance() { if (s1 == null) { s1 = new Singleton(); } return s1; } } /* 懒汉式的优点:按需加载,可以节约性能 懒汉式的缺点:无法保证线程安全 如何解决懒汉的线程安全问题? 加sychronized(静态对象的同步对象是当前类的反射对象,具有唯一性,静态jvm的一次运行中,每一个类都会先获取这个类的反射对象,反射对象用于后续的类加载,反射对象只有一个) 可以通过加悲观锁的方式解决线程安全问题,但是无法保证效率 */
-
饿汉:预加载
public class Singleton { private static Singleton st = new Singleton(); private Singleton() { } public static Singleton getInstance() { return st; } } /* 饿汉式的优点:在使用者调用getInstance方法的时候方法早就完成实例化,就无需考虑线程安全问题 饿汉式的缺点:不具备延迟加载的特性 */
-
双重检查锁
public class Single { //避免指令重排序 private static volatile Single single; private Single() { } public static Single getInstance() { //第一重检查,假设对象已经被实例化, //其他线程获取的时候发现引用不为空,就不会再获取同步对象锁,性能提升了, //只有在线程第一次访问时才会获取同步对象锁 if (single == null) { synchronized (Single.class) { //第二重检查,保证线程安全 if (single == null) { single = new Single(); } } } return single; } } /* 本身是懒汉,保证了线程的可见性,禁止了指令重排序 优点:延迟加载,线程安全,性能较好 对象创建的步骤: 1.new分配对象空间 2.调用构造方法,完成对象的初始化 3.将内存地址赋值给引用 jvm的指令重排序可能导致构造函数对对象初始化完成之前就已经对引用进行赋值 其他线程在判断instance不为空时就直接获取使用,发现对象没有初始化,很容易出现空指针异常 所以使用volatile解决指令重排序的问题 */
-
-
工厂方法模式
public class PhoneFactory { public Phone create(String type){ if (type.equals("IPhone")){ return new IPhone(); }else if (type.equals("MPhone")){ return new MPhone(); }else return null; } }
-
工厂模式
-
代理模式
-
-
Maven
- 一个jar包管理工具
- 本地仓库+云端仓库镜像
- 工具
- D:\Development Environment\Maven\Iweb\apache-maven-3.5.0
- D:\Development Environment\Maven\Iweb\repository
- 新建Maven项目----Pom
- 打包:package
-
Git
-
新特性之Lambda和Stream
- JDK1.8新特性 - 知乎 (zhihu.com)
- Stream关注对数据的运算,和cpu打交道
- 自己不存储对象
- 不改变源对象,返回一个新的stream
- stream是延迟执行的,只有需要结果时才执行
- 执行流程
- 实例化:创建stream,通过数据源获取stream
- 中间操作:过滤,映射—对数据源的数据进行业务处理
- 终止操作:一旦执行终止操作,就执行中间操作操作链条并产生结果,之后不会再被使用
- 创建:Collection在JDK8中拓展了stream和paralleStream