scala是运行在 JVM 上的多范式编程语言,同时支持面向对象和面向函数编程,scala最终被编译为.class文件,运行在JVM虚拟机中,其实本质上还是java, 所以在scala和java可以互调双方的api,兼容Java,可以访问庞大的Java类库。
Scala是函数式和面向对象编程的结合,而Java是面向对象编程语言。
用Scala编写的代码更短、更紧凑。在Java中,代码是长格式的。
(1)变量
(2)方法函数
(3)接口
(4)对字符串的支持 :scala采用三个双引号“”“支持换行字符串,Java需要采用“+”进行字符串的连接。
方法 描述
(1)定义方式:val 语句 定义函数;def 语句 定义方法。
(2)属性不同:
1、定义方式:
//方法的定义
scala> def m1(x: Int) = x + 1
m1: (x: Int)Int
#方法是一个以def开头的带有参数列表(可以无参数列表)的一个逻辑操块,
#这正如object或者class中的成员方法一样。(上面有定义说明)
//函数的定义
scala> val f1 = (x : Int) => x + 1
f1: Int => Int = $$Lambda$1036/961708482@1573e8a5
#函数是一个赋值给一个变量(或者常量)的匿名方法(带或者不带参数列表),
#并且,通过 =>转换符号 跟上逻辑代码块的一个表达式。
# =>转换符号 后面的逻辑代码块的写法与method的body部分相同。
2、相互转换:
scala> def add(x:Int,y:Int)=x+y
add: (x: Int, y: Int)Int
scala> val a = add _
a: (Int, Int) => Int = <function2>
·(1)元组,val a = (1, “张三”, 20, “北京市”) :可包含一组不同类型的值,_1表 示访问第一个元素
·(2)变长数组,scala.collection.mutable.ArrayBuffer:增+=、删-=、追加++=, array.sum/max/min/sorted
· (3)可变Map,scala.collection.mutable.Map: 增map+=(“w” ->35),删map -=“w”,改map(“li”)=50,查map(“zh”)
·(4)可变Set,scala.collection.mutable.Set:不重复 无顺序,增+=、删-=、追 加++=,
·(5)可变列表,scala.collection.mutable.ListBuffer,可重复 有顺序,增+=、删 -=、++=加list,
Scala元组将固定数量的项目组合在一起,以便它们可以作为一个整体传递。
元组可以容纳不同类型的对象( 与数组或列表不同),但它们也是不可变类型。
//定义一个数组
scala> val array=Array(1,2,3,4,5)
array: Array[Int] = Array(1, 2, 3, 4, 5)
//定义一个没有名称的函数----匿名函数
scala> array.map(x=>x*10)
res1: Array[Int] = Array(10, 20, 30, 40, 50)
含义:方法可以定义多个参数列表,用较少的参数列表,调用较多参数列表的方法时,会产生一个新的函数,该函数接收剩余的参数列表作为其参数。这被称为柯里化。
柯里化(Currying)是一种,将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
柯里化,是把接受多个参数的函数,变换成,接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
柯里化,是函数式编程的一个重要概念。它既能减少代码冗余,也能增加可读性。另外,附带着还能用来装逼。
curry化最大的意义在于, 把多个参数的function等价转化成, 多个单参数function的级联,这样所有的函数就都统一了,方便做lambda演算。
在scala里,curry化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,curry化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演。这就是为什么 foldLeft这种函数的定义都是curry的形式
# 定义2个整数相乘运算
def multiplie2par(x:Int,y:Int)=x*y
# 使用柯里化技术,
# 将上述2个整数的乘法函数,改修为接受一个参数的函数,
# 该函数 返回 是一个以原有第二个参数为参数的函数。
def multiplie1par(x:Int)=(y:Int)=>x*y
# 说明:
# multiplie1par(x:Int) 为接收一个参数的新等价函数,
# (y:Int)=>x*y 则是新等价函数的返回体,它本身就是一个函数(严格来说应该是一个匿名函数),
# 参数为 除等价新函数的参数外原函数剩余的参数。
进一步简化为:
def multiplie1par1(x:Int)(y:Int)=x*y
三个整数乘法的函数
def multiplie3par(x:Int,y:Int,z:Int)=x*y*z
def multiplie3par1(x:Int)=(y:Int,z:Int)=>x*y*z
def multiplie3par2(x:Int)(y:Int)(z:Int)=x*y*z
def multiplie3par3(x:Int)(y:Int)=(z:Int)=>x*y*z
# 编译执行的结果都是一样,调用不同形式参数过程略有不同,
# 直接参入三个参数的一步到位就可以得到运算结果,而
# 传入1或者2个参数的需要分步骤再传入第2/3或者第3个参数才能求出三个整数相乘的结果,
# 很好地体现了延迟执行或者固定易变因素等方面能力。
def getAddress(a:String):(String,String)=>String={
(b:String,c:String)=>a+"-"+b+"-"+c
}
scala> val f1=getAddress("china")
f1: (String, String) => String = <function2>
scala> f1("beijing","tiananmen")
res5: String = china-beijing-tiananmen
//这里就可以这样去定义方法
def getAddress(a:String)(b:String,c:String):String={
a+"-"+b+"-"+c
}
//调用
scala> getAddress("china")("beijing","tiananmen")
res0: String = china-beijing-tiananmen
//之前学习使用的下面这些操作就是使用到了柯里化
List(1,2,3,4).fold(0)(_+_)
List(1,2,3,4).foldLeft(0)(_+_)
List(1,2,3,4).foldRight(0)(_+_)
函数里面,引用外面类成员变量叫作闭包
var factor=10
val f1=(x:Int) => x*factor
//定义的函数f1,它的返回值是依赖于不在函数作用域的一个变量
//后期必须要要获取到这个变量才能执行
//spark和flink程序的开发中大量的使用到函数,函数的返回值依赖的变量可能都需要进行大量的网络传输获取得到。
//这里就需要这些变量实现序列化进行网络传输。
scala中是没有Java中的静态成员的。如果将来我们需要用到static变量、static方法,就要用到scala中的单例对象object
在for表达式中可以添加if判断语句,这个if判断就称为守卫
scala> for(i <- 1 to 10 if i >5) println(i) 6 7 8 9 10
Scala提供的隐式转换和隐式参数功能,是Java等编程语言所没有的功能。
scala允许开发人员自定义类型转换规则,将两个无关的类型通过编程手段让他们自动转换。
隐式转换:在 Scala 编译器进行类型匹配时,如果找不到合适的类型就会编译失败,此时会在当前的环境中自动推导出合适的类型,从而完成编译。
核心:就是定义一个使用 implicit 关键字修饰的方法 ,实现把一个原始类转换成目标类,进而可以调用目标类中的方法。
作用:简化编程,调用方法时,不需要向隐式参数传参,Scala 会自动在其作用域范围内寻找隐式值,并自动传入。通过隐式转换,可以在不改变代码的情况下,扩展某个类的功能。
隐式函数:使用implicit关键字声明的函数称之为隐式函数
object ScalaImplicit {
def main(args: Array[String]): Unit = {
//定义隐式函数 让Double 类型的变量自动转换为int类型
implicit def transform( d : Double ): Int = {
d.toInt
}
var d : Double = 2.0
val i : Int = d
println(i)
}
}
隐式参数,指的是在函数或者方法中,定义一个用implicit修饰的参数, 此时Scala会尝试找到一个指定类型的用implicit修饰的参数,即隐式值,并注入参数。
所有的隐式转换和隐式参数必须定义在一个object中
object ImplicitConversion {
def main(args: Array[String]): Unit = {
//隐式值/变量
implicit val dd : Double = 2.0
//隐式参数
def transform( implicit d : Double = 3.0 ) = {
d.toInt
}
//值调用顺序:隐式值 -> 隐式参数默认值 -> 前两者都没有 报错
println(transform()) //结果:3
//方法调用时,使用小括号会导致隐式值无法转递 所以这里调用的是隐式参数的值
println(transform)//结果:2
//不使用小括号可以传递隐式值
}
}
}
在Scala2.10后提供了隐式类,可以使用implicit声明类,隐式类的非常强大,同样可以扩展类的功能,在集合中隐式类会发挥重要的作用。
其所带的构造参数有且只能有一个
隐式类必须被定义在“类”或“伴生对象”或“包对象”里,即隐式类不能是顶级的。
object ScalaImplicit {
def main(args: Array[String]): Unit = {
val emp = new Emp()
emp.insertUser()
}
class Emp {
}
//将一个类Emp 变成另外一个类 User
implicit class User( emp : Emp) {
def insertUser(): Unit = {
println("insert user...")
}
}
}
JVM(java虚拟机Java Virtual Machine
), 是一种抽象化的计算机, 在运行时负责将Java程序的.class文件解释成特定的机器码进行运行,有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。具有跨平台、跨语言的特性。
JVM可以将Java语言通过各种不同的解释器翻译成各个平台(windows、linux等)能读懂的语言因此可以跨平台。
JVM ,用以把Java语言编译后的.class文件翻译成系统能读懂的机器码, 如果别的语言也翻译成了.class二进制字节码文件,也可以在JVM里运行,如Scala。
类加载器负责加载class文件,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构,并且ClassLoader只负责文件的加载,至于它是否可以运行,则由Execution Engine决定
从类被加载到虚拟机内存中开始,到卸御出内存为止,它的整个生命周期分为7个阶段:加载--验证--准备--解析--初始化--使用--卸载
。其中验证、准备、解析三个部分统称为连接。
1)加载:
2)验证:确保加载的类符合JVM规范与安全。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
3)准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0;
4)解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
5)初始化:为类的静态变量赋予正确的初始值
6)使用:正常使用
7)卸载:GC把无用的对象从内存中卸载
1)Bootstrap ClassLoader
:负责加载JAVA_HOME中jre/lib/rt.jar里所有的 class,由 C++ 实现,不是 ClassLoader 子类。
2)Extension ClassLoader
:负责加载Java平台中扩展功能的一些 jar 包,包括JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的 jar 包。
3)App ClassLoader
:负责加载 classpath 中指定的 jar 包及目录中 class。
4)Custom ClassLoader
:属于应用程序根据自身需要自定义的 ClassLoader,如 Tomcat、jboss 都会根据 J2EE 规范自行实现 ClassLoader。
先检查,再加载
检查类的顺序:是自底向上,从 Custom ClassLoader
到 BootStrap ClassLoader
逐层检查,只要某个 Classloader 已加载就视为已加载此类,保证此类只所有 ClassLoader 加载一次。
JVM在加载类时
默认采用的是双亲委派机制:
就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
JVM运行时数据区域:由5块部分组成,分别是堆,方法区,虚拟机栈,本地方法栈,以及程序计数器组成。
可以根据内存是否线程共享划分成:线程私有区域/ 线程共享区域。
堆内存:存放的是new的对象,对象实例和数组等,垃圾收集器就是收集这些对象,然后根据GC算法回收。
一个JVM实例只存在一个堆内存,堆内存也是Java内存管理的核心区域,所有的线程共享Java堆内存
JVM启动的时候即被创建,堆内存的大小是可调节的。
方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆内存是Gc执行垃圾回收的重点区域
新生代:老年代 = 1:2
新生代中,suvivor(幸存者):eden0(from):eden1(to 伊甸园)= 8:1:1
堆内存GC流程 ,对象如何晋升到老年代
当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC,以收集新生代的垃圾,存活下来的对象,则会转移到 Survivor区。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。
堆内存什么要分成新生代,老年代,持久代
如果没有Survivor
,Eden区每进行一次Minor GC
,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生
,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
在jdk1.8当中已经不存在永久代这一说了,取而代之的是元数据区,
方法区:存储已经被虚拟机加载的类结构信息、常量、静态变量等;
各个线程共享的内存区域,在JVM启动的时候被创建, 关闭JVM就会释放这个区域的内存。
方法区的大小决定了系统可以保存多少个类,如果定义太多类,加载大量的第三方的Jar包,Tomcat部署过多工程,导致方法区溢出,虚拟机同样会抛出内存溢出OOM:PermGenspace或者Metaspace
虚拟机栈,主管Java程序的运行(主要是方法的执行),保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分中间结果,并参与方法的调用和返回。
每个线程创建时都会创建一个虚拟机栈,
内部保存一个个栈帧,对应着一次次的Java方法调用
生命周期和线程的—致,线程被销毁,虚拟机栈也就随之销毁,不存在垃圾回收问题,
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,每个方法执行的同时都会创建一个栈帧,用于存储局部变量表等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右
栈内存溢出
如果线程请求的栈深度
大于虚拟机所允许的最大深度
,将抛出StackOverflowError
异常,方法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory
异常。(线程启动过多)
参数 -Xss 去调整JVM栈的大小
作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,目前该方法使用的越来越少了,除非是与硬件有关的应用
(1)几种垃圾收集器:
Serial收集器
: 单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。ParNew收集器
: Serial收集器的多线程版本,也需要stop the world,复制算法Parallel Scavenge收集器
: 新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。Serial Old收集器
: 是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。Parallel Old收集器
: 是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。(2)CMS收集器和G1收集器的区别:
自动转换:char–> byte–>short–>int–>long–>float–>double
所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类 Number 的子类。
String s1 = "abc"; // 常量池
String s2 = new String("abc"); // 堆内存中
System.out.println(s1==s2); // false两个对象的地址值不一样。
System.out.println(s1.equals(s2)); // true
Java 中int 和 Integer 的区别
Java中String、StringBuffer 和 StringBuilder 的区别
String
;单线程操作大量数据用StringBuilder
;多线程操作大量数据,用StringBuffer
。ArrayList 和 LinkedList 的区别
ArrayList 是 List 接口的一种实现,它是使用数组来实现的。
LinkedList 是 List 接口的一种实现,它是使用链表来实现的。
ArrayList 遍历和查找
元素比较快。LinkedList 遍历和查找元素比较慢。
LinkedList 添加、删除
元素比较快。ArrayList 添加、删除元素比较慢。
浅拷贝、深拷贝
基本数据类型
的特点:直接存储在栈(stack)中的数据
引用数据类型
的特点: 存储的是该对象在栈中引用
,真实的数据存放在堆内存
里
引用数据类型,在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体
。
深拷贝和浅拷贝,针对
引用数据类型。
public class Variable{
static int allClicks=0; // 类变量
String str="hello world"; // 实例变量
public void method(){
int i =0; // 局部变量
}
}
1)类变量 (静态变量):声明在类中方法体之外,用 static 修饰,属于整个类。可通过对象名或类名来调用。
2)实例变量(成员变量):声明在类中方法体之外,没有 static 修饰,在创建对象的时候实例化。可以被类中方法、构造方法和特定类的语句块访问。
3)局部变量:在方法、构造方法或语句块中定义的变量 。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
类变量在方法区中、实例变量在堆上 ,局部变量在栈上
OOP: object -oriented-proramming
面向对象编程
面向对象编程的本质就是:以类的方式组织代码,以对象的组织(封装)数据。
三大特性:
1.父类引用指向子类的对象
2.把子类转换为父类,向上转型;
3.把父类转换为于类,向下转型;强制转换,((sudent)s2).eat()
。
方法:
方法名与返回值类型相同
,但参数(个数、类型、位置)不同
,目的在于相同功能的方法使用一个名字,便于调用。@override
,子类重写父类的方法,方法签名(方法名称、参数和返回类型) 要一样,重写必须要有继承高级:
没有具体的执行代码
,那么这个方法为抽象方法, 用abstract
修饰,定义了子类必须实现的接口规范,实现代码的重用一个类却可以实现多个接口,但只能继承一个抽象类
读写属性,传递数据,枚举属性
<1> 访问权限:public、default、protected、private
类(外部类),有 2 种访问权限:public、default。
方法和变量,有 4 种访问权限:public、default、protected、private。
public
: 表示任何地方的其他类都能访问。private
: 表示只有自己类能访问。default
:同一个包的类可以访问。protected
:表示同一个包的类可以访问,其他的包的该类的子类也可以访问。<2> 修饰符:abstract、static、final
this、super
1)算术运算符、2)关系运算符、3)位运算符、4)逻辑运算符、5)赋值运算符、6)其他运算符
循环结构 : for, while 及 do…while
Math.floor :向下取整。Math.ceil :向上取整。Math.round :四舍五入取整
Map接口和Collection接口是所有集合框架的父接口:
集合和数组的区别
数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
数组: int[] ns={1,2,3,4} ; For(int n :ns) ; int[][] ns={{1,2,3,4},{5,6,7,8}}
特点:在内存中顺序存储,是有限个相同类型
的变量所组成的有序集合
,下标从0开始,一直到数组长度-1。
时间复杂度:读取元素、更新元素:O(1)
;插入操作、删除操作:O(n)
。
优点:按照索引查询
元素的速度很快;按照索引遍历数组也很方便。
劣势:数组大小在创建后就确定
了,无法扩容;数组只能存储一种类型
的数据;添加删除元素
的操作很耗时间,因为要移动其他元素。
应用场景:读操作多、写操作少的场景
链表: 是一种在物理上非连续、非顺序
的数据结构,由若干节点所组成
。每一个节点又包含两部分,一部分是存放数据
的变量data;另一部分是指向下一个节点的指针
next。
双向链表:每一个节点除了拥有data
和next指针
,还拥有指向前置节点
的prev指针。
插入删除
时, O(1)
的时间复杂度
优点:不需要初始化容量;可以添加任意元素
;插入和删除的时候只需要更新引用。
缺点:含有大量的引用
,占用的内存空间大;查找元素
需要遍历整个链表,耗时。
应用场景:尾部频繁插入、删除元素
数组与链表区别:
数组:顺序存储
,在内存中占用了连续完整的存储空间。
链表:随机存储
,采用了见缝插针的方式,链表的每一个节点分布在内存的不同位置,依靠next指 针关联起来。这样可以灵活有效地利用零散的碎片空间。
查找元素:链表不像数组那样可以通过下标快速进行定位
,只能从头节点开始向
后一个一个节点逐一查找
。O(n)。
数组的优势,在于能够快速定位
元素,适用于读多写少
的场景
链表的优势,在于能够灵活插入删除
操作,如果需要在尾部频繁插入、删除元素,用链表更合适一些
和弹夹很相似,先进后出,是一种线性
数据结构,可以用数组来实现,也可以用链表来实现。
入栈和出栈,只影响最后一个元素,不涉及其他元素的整体移动,所以无论是以数组还是以链表实现,入栈出栈的时间复杂度都是O(1)
。
应用场景:逆流而上追溯“历史”
,实现递归的逻辑,就可以用栈来代替,因为栈可以回溯方法的调用链;面包屑导航
,用户在浏览页面时,轻松地回溯到上一级或更上一级页面。
和单行隧道很相似,先进先出,是一种线性
数据结构,可以用数组来实现,也可以用链表来实现
应用场景:按照“历史”顺序,把“历史”重演一遍
多线程中,争夺公平锁的等待队列,就是按照访问顺序来决定线程在队列中的次序的。
双端队列(deque):把栈和队列的特点结合起来,既可以先入先出,也可以先入后出.
树,是一种非线性
结构,由n(n>0)个有限结点组成有层次关系的集合
。树的最大层级数,被称为树的高度或深度。
二叉树:每个结点最多含有2个子树。可能只有1个,或者没有孩子节点。
满二叉树: 每一个分支都是满的,都有两个子结点的二叉树。
-完全二叉树: 条件没有满二叉树 苛刻, 完全二叉树只需保证,最后一个节点之前的节点都齐全
即可。
二叉查找树:用以进行查找
操作,要求左子树小于父节点,右子树大于父节点
,正是这样保证了二叉树的有序性。 性质:
平衡二叉树:也称AVL树,当且仅当任何结点的两棵子树的高度差不大于1
的二叉树。Java中HashMap的红黑树就是平衡二叉树!!!
B树:一种对读写优化的自平衡二叉树
,在数据库的索引中常见的BTREE就是自平衡二叉树。
B+树:B+树是应文件系统
所需而产生的B树的变形树。
索引部分
,结点中仅含有其子树根结点中最大(或最小)关键字遍历:
从更宏观的角度来看,二叉树的遍历归结为两大类。
深度优先遍历: 偏向于纵深, 一头扎到底
的访问方式。可
广度优先遍历: 就是二叉树按照从根节点到叶子节点的层次关系
,一层一层横向遍历
各个节点
二叉堆
优先队列
一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。
节点之间的关系是任意的,图中任意两个数据元素之间都有可能相关。
散列表,提供了键(Key)和值(Value) 的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)
。
jdk8中,Java中经典的HashMap,以 数组+链表+红黑树
构成。
哈希函数
按照数组长度进行取模运算
。写操作
Entry
填充到数组下标5的位置。哈希冲突
解决哈希冲突的方法,
为了提升插入和查找的效率,HashMap会把Entry 的链表转化为红黑树
这种数据结构。
根据时间复杂度的不同,主流的排序算法可以分为3大类。
https://www.processon.com/view/609b2f875653bb147747da16?fromnew=1
1、冒泡排序:从第一个起,相邻两个比较,大的依次往后移动。第一次排序,找到最大的放最后,第二次次排序,找到次大的放倒数第二位置,一直放到第一个位置。
2、选择排序:先选择全部中的最小的,放第一个位置;再选择从第二个到最后中最小的,放第二个位置;依次从后面选择最小的放在前面。
3、插入排序:摸牌,一个有序表和一个无序表,每次从无序表中取第一个元素,依次与有序表元素比较,插入到有序表中的适当位置。
4、希尔排序:插入排序的改进,缩小增量排序。先设置初始增量gap=length/2,依次从头开始,将0号元素与加上增量的元素分为一组;对分好组的每一组数据完成插入排序;减小增量为原来的一半,继续分组后排序,直到增量最小减为1
如10个数: 第一轮,增量gap=length/2=5,分为5组,1、6元素一组,2、7一组,3、8一组插入排序。
第二轮,增量gap=gap/2=2,分为2组,1、3、5、7、9元素一组,2、4、6、10一组进行插入排序
第三轮,增量gap=gap/2=1,分为2组,
5、快速排序:冒泡排序的改进,先设一个分界值,将大于分界值的放到到数组右边,小于分界值的数据放到数组的左边;然后,左边数组和右边数组又各自设一个分界值,递归分成左右两大小组, 直到每组不能划分。当左侧和右侧两个部分的数摇排完序后整个数姐的排序也就完成了。
6、基数排序(桶排序):将每个元素按照其个十百千等位数中的数字,放入对应1到9号桶(数组);从最低位开始,依次进行一轮排序,最多进行元素最高位轮排序,数列就变成一个有序序列.
public static void quickSort(int[] arr,int left, int right) {
int l = left; //左下标
int r = right; //右下标
int pivot = arr[(left + right) / 2]; //pivot 中轴值
int temp = 0; //临时变量,作为交换时使用
// todo 1、交换左右两边的数,让比pivot的值小放到左边,比pivot 值大放到右边
while( l < r) {
while( arr[l] < pivot) {//在pivot的左边一直找,找到大于等于pivot值,才退出
l += 1;
}
while(arr[r] > pivot) { //在pivot的右边一直找,找到小于等于pivot值,才退出
r -= 1;
}
if( l >= r) { //如果l >= r说明pivot 的左右两的值,
break; // 说明已经按照左边全部是小于等于pivot值,右边全部是大于等于pivot值
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完后,发现这个arr[l] == pivot值 相等 r--, 前移
if(arr[l] == pivot) {
r -= 1;
}
//如果交换完后,发现这个arr[r] == pivot值 相等 l++, 后移
if(arr[r] == pivot) {
l += 1;
}
}
// todo 2、交换到l==r, 防止栈溢出,设置l++, r--, 交叉,就不会执行上面的while否
if (l == r) {
l += 1;
r -= 1;
}
// todo 3、 左右两边递归快速排序
if(left < r) { //向左递归
quickSort(arr, left, r);
}
if(right > l) { //向右递归
quickSort(arr, l, right);
}
}
public static int binarySearch(int[] arr, int left, int right, int findVal) {
// 当 left > right 时,说明递归整个数组,但是没有找到
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) { // 向 右递归
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) { // 向左递归
return binarySearch(arr, left, mid - 1, findVal);
} else {
return mid;
}
}
https://www.processon.com/view/5f3f16a85653bb06f2ddd549?fromnew=1
进程:在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。系统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是一个比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
进程要想执行任务,必须得有线程,进程至少要有一条线程
并发编程三要素
1)原子性:指一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
2)可见性:指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
3)有序性:程序的执行顺序按照代码的先后顺序来执行。
1)发挥多核CPU的优势,多线程可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
2)防止阻塞,从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。
3)便于建模,假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
1)新建(new Thread):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1=new Thread();
2)就绪(runnable):线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();
3)运行(running):线程获得CPU资源正在执行任务(run()方法
),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
4)堵塞(blocked):由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
sleep(long t)
方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。wait()
方法。(调用motify()方法回到就绪状态)suspend()
方法。(调用resume()方法恢复)5)死亡(dead):当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1)继承Thread
类,重写run()方法
2)实现Runnable
接口,重写run()方法
3)实现Callable
接口,重写call()方法,有返回值
4)线程池方式创建:Java通过Executors提供四种线程池:
1 newCachedThreadPool 创建⼀个可缓存线程池, 如果线程池⻓度超过处理需要,可灵活回收空闲线程,若⽆可 回收,则新建线程。
2 newFixedThreadPool 创建⼀个定⻓线程池, 可控制线程最⼤并发数,超出的线程会在队列中等待。
3 newScheduledThreadPool 创建⼀个定⻓线程池, ⽀持定时及周期性任务执⾏。
4 newSingleThreadExecutor 创建⼀个单线程化的线程池, 它只会⽤唯⼀的⼯作线程来执⾏任务,保证所有任务 按照指定顺序(FIFO, LIFO, 优先级)执⾏。
1)采用实现Runnable、Callable接口的方式创建多线程。
2)使用继承Thread类的方式创建多线程
3)Runnable和Callable的区别
call()
,Runnable规定(重写)的方法是run()
。sleep方法和wait方法都可以用来放弃CPU一定的时间,
类:sleep()来自thread,wait()来自object();
锁:sleep()不释放锁,wait()释放锁;sleep()时间到了会自动恢复,wait()可以使用notify()直接唤醒
使用范围:sleep可以在任何地方使用;wait,notify和notifyAll,只能在同步控制方法或者同步控制块里面使用,
① 来自不同的类,分别是,sleep来自Thread类,wait来自Object类。 sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
② 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 sleep不让出系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用, 要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来, 如果时间不到只能调用interrupt()强行打断。 Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
③ 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。 synchronized(x){ x.notify() //或者wait() } 两者的线程状态都从运行—> 阻塞,不同的是sleep方法在阻塞的同时还带着锁,wait方法在阻塞
线程安全问题:指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等。
线程安全问题发生的条件:
线程安全问题的解决思路:
1)乐观锁: 对于并发间操作产生的线程安全问题,持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2)悲观锁: 对于并发间操作产生的线程安全问题,持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
可重入锁STFW得到以下两种主流解释
解释一:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
解释二:可重入锁又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。
1)死锁,
2)饥饿,
3)活锁,
阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞。如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。
当我们使用synchronized
或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
解决方法:
可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
参考
JVM提供了synchronized
关键字来实现对变量的同步访问,以及用wait和notify
来实现线程间通信。
在jdk1.5以后,JAVA提供了Lock类
来实现和synchronized
一样的功能,并且还提供了Condition
来显示线程间通信。
Lock类是Java类来提供的功能,丰富的api使得Lock类的同步功能比synchronized的同步更强大。
Lock lock = new ReentrantLock();
。volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。JMM(Java内存模型)是围绕并发过程中如何处理可见性、原子性和有序性这3个特征建立起来的,而volatile可以保证其中的两个特性。
Java提供了volatile关键字
来保证可见性。
共享变量被volatile修饰
时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。CAS(compare and swap)是解决多线程并行情况下使用锁造成性能损耗的一种机制。
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知ThreadLocal的作用是∶提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
1、同步方法,使用 synchronized关键字,可以修饰普通方法、静态方法,以及语句块。
2、同步代码块,用synchronized关键字修饰语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
3、使用特殊域变量(volatile)实现线程同步。
4、使用重入锁实现线程同步,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
5、使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。
解决思路:
利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:线程复用;控制最大并发数:;管理线程。
java中的线程池是通过 Executor
框架实现的,该框架中用到了 Executor, Executors,ExecutorService, ThreadPoolExecutor
这几个类。
Future模式
场景比如:外卖。
使用Future模式
网络编程是指:编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。
java.net 包中提供了两种常见的网络协议的支持:
TCP:TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
UDP:UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。
Socket 编程:套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。
java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。
同步和异步:同步和异步是针对应用程序和内核的交互而言的。
以银行取款为例:
阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式。
以银行取款为例:
输入流就是从外部文件输入到内存,输出流主要是从内存输出到文件。
IO 流主要分为字符流
和字节流
。
InputStream
和 OutputStream
,它们子类 FileInputStream
,FileOutputStream
,BufferedOutputStream
等。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
java 中的阻塞式方法是指在程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如 read()和readLine()方法。
是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从 文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。
按照流的方向:输入流inputStream、输出流outputStream
按照实现功能分:
按照处理数据的单位: 字节流和字符流。
字节流继承于 InputStream 和 OutputStream, 字符流继承于 InputStreamReader 和 OutputStreamWriter 。
字节流的操作,不会经过缓冲区(内存),而是直接操作文本本身的;字符流的操作,会先经过缓冲区(内存),然后通过缓冲区再操作文件以字节为单位输入输出数据,
字节流按照 8 位传输 以字符为单位输入输出数据,字符流按照 16 位传输。
IO 操作包括:对硬盘的读写、对 socket 的读写以及外设的读写。 当用户线程发起一个 IO 请求操作,内核会去查看要读取的数据是否就绪
也就是说 一个完整的 IO 读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别
Java 中传统的 IO 都是阻塞 IO,比如通过 socket 来读数据,调用 read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在 read 方法调用那里,直到有数据才返回;而如果是非阻塞 IO 的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,
传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
对于 NIO,它是非阻塞式,核心类:
(1)Buffer 为所有的原始类型提供 (Buffer)缓存支持。
(2)Charset 字符集编码解码解决方案 、
(3)Channel 一个新的原始 I/O 抽象,用于读写 Buffer 类型,通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。
1)传统 IO 一般是一个线程等待连接,连接过来之后分配给processor线程,processor 线程与通道连接后,如果通道没有数据过来就会阻塞(线程被动挂起)不能做别的事情。
NIO 则不同,首先,在 selector 线程轮询的过程中就已经过滤掉了不感兴趣的事件,其次,在 processor处理感兴趣事件的 read 和 write 都是非阻塞操作即直接返回的,线程没有被挂起。
2)传统 io 的管道是单向的,nio 的管道是双向的。
3)不管传统 io 还是 nio 都需要read 和 write 方法,这些都是 java 程序调用的而不是系统帮我们调用的,nio2.0 里这点得到了改观,即使用异步非阻塞 AsynchronousXXX 四个类来处理。
同步:java 自己去处理 io。
异步:java 将 io 交给操作系统去处理,告诉缓存区大小,处理完成回调。
阻塞:使用阻塞 IO 时,Java 调用会一直阻塞到读写完成才返回。
非阻塞:使用非阻塞 IO 时,如果不能立马读写,Java 调用会马上返回,当 IO 事件分发器通知可读写时在进行读写,不断循环直到读写完成。
BIO:同步并阻塞
NIO:同步非阻塞
AIO:异步非阻塞
应用场景:并发连接数不多时采用 BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择 NIO 或 AIO,更好的建议是采用成熟的网络通信框架 Netty。
序列化就是一种用来处理对象流的机制,将对象的内容进行流化。可以对流化后的对象进行读写操作,可以将流化后的对象传输于网络之间。
序列化是为了解决,在对象流读写操作时所引发的问题
序列化的实现:
1、 PrintStream 类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成PrintStream 后进行输出。它还提供其他两项功能。与其他输出流不同,PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream
2、BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符串的高效写入。通过 write()方法可以将获取到的字符输出,然后通过 newLine()进行换行操作。BufferedWriter 中的字符流必须通过调用 flush 方法才能将其刷出去。并且 BufferedWriter 只能对字符流进行操作。如果要对字节流操作,则使用 BufferedInputStream
3、PrintWriter 的 println 方法自动添加换行,不会抛异常,若关心异常,需要调用 checkError方法看是否有异常发生,PrintWriter 构造方法可指定参数,实现自动刷新缓存(autoflush)
在 java.io 包中主要由 4 个可用的 filter Stream。两个字节 filter stream,两个字符 filter stream.分别是 FilterInputStream, FilterOutputStream, FilterReader and FilterWriter.这些类是抽象类,不能被实例化的。
1). 实现 Cloneable 接口并重写 Object 类中的 clone()方法;
2). 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克 隆