以下内容不分先后顺序,均为个人学习期间做的笔记
方法设计
-
单元:一个类或者一组类(组件)
- 类采用名次结构
- 动词过去式+名次
- ContextRefreshedEvent
- 动词ing + 名词
- InitializingBean
- 形容词 + 名次
- ConfigurableApplicationContext
- 动词过去式+名次
- 类采用名次结构
-
执行:某个方法
- 方法命名:动词
- execute
- run
- …
- 方法参数:名词
- 异常:
- 根(顶层)异常
- Throwable
- 检查异常(checked):Exception
- 非检查异常(unchecked):RuntimeException
- Error
- Throwable
- 根(顶层)异常
在Java中,构造异常对象是”十分”耗时的,其原因是在默认情况下,创建异常对象时会调用父类
Throwable
的fillInStackTrace()
方法生成栈追踪信息,JDK中的源码如下:public synchronized Throwable fillInStackTrace() { if (stackTrace != null || backtrace != null /* Out of protocol state */ ) { fillInStackTrace(0); // native方法 stackTrace = UNASSIGNED_STACK; } return this; }
- 方法命名:动词
我们在开发业务系统的过程中一般都会使用异常机制来实现错误处理逻辑,这些异常通常都可以分成两大类:
- 业务异常
这些是我们自定义的、可以预知的异常,抛出这种异常并不表示系统出了问题,而是正常业务逻辑上的需要,例如用户名密码错误、参数错误等。 - 系统异常
往往是运行时异常,比如数据库连接失败、IO失败、空指针等,这种异常的产生多数表示系统存在问题,需要人工排查定位。
其实方法非常简单,在我们自定义异常时,只需要重写父类的一个带有4个参数的构造方法即可,此方法在Exception
和RuntimeException
类中都存在:
protected RuntimeException(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
这几个参数的意义如下:
- message
异常的描述信息,也就是在打印栈追踪信息时异常类名后面紧跟着的描述字符串 - cause
导致此异常发生的父异常,即追踪信息里的caused by
- enableSuppress
关于异常挂起的参数,这里我们永远设为false
即可 - writableStackTrace
表示是否生成栈追踪信息,只要将此参数设为false
, 则在构造异常对象时就不会调用fillInStackTrace()
方法入参设计:
-
集合类操作,请不要直接利用参数,建议copy一份出来进行操作并返回
-
对自己严格,所有的返回接口类型禁止返回null,对别人宽容,要做null判断
-
集合方法入参:
- 如果能用 Iterable 尽量用
- 其次是 Collection再者是 List 或 Set
- 禁止使用具体类型,比如:ArrayList,LinkedHashSet
方法返回值设计:
- 方法返回值设计可以使用Optional
- 返回类型需要抽象(强类型),除Object,抽象返回的类型意义在于调用方容易处理。越具体,越难通用
- 如果返回的类型是集合的话,Collection 优于 List 或 Set
- 如果不考虑写操作,Iterable 优于 Collection
- 尽可能返回Java集合框架内的接口,尽量避免数组
- Collection 比较 [] 而用,拥有更多的操作方法,比如add
- Collection 接口返回时,可以限制只读,而 [] 不行
- 确保集合返回接口只读
- 如果需要非只读集合返回的话,那么确保返回快照
方法参数设计:
Effective Java 建议不要超过四个参数
Java 8 Lambda 告诉用户,最多使用三个
Runnable(Action) 零个
Consumer 一个
Function BiConsumer 两个
BiFunction 三个
泛型设计
使用场景
编译时强类型检查、避免类型强转、实现通用算法
Java 泛型属于编译时处理,运行时类型擦写
- 例如Couple[] couple = new Couple[5] ;类型查出后couple的类型是Couple[]。
泛型程序设计:可以被很多不同类型的对象所重用。比那些直接使用Object变量,然后强制类型转换的代码具有更好的安全性和可读性
在Java中,使用变量E表示集合的元素类型,K和V分别表示键值对格式,T表示类型
-
泛型类的声明在类名后加上,如public class Pari
-
泛型方法即可以在普通类中,也可以在泛型类中,定义方法是在方法名前面加,说明该方法是泛型方法
泛型的约束和局限性:
-
不能用类型参数来代替基本类型。就是没有Pair,只有Pair。当然主要是原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double的值。
-
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有类型查询只产生原始类型。
如:if(a instanceof Pari)是错误的,因为只能查询原始类型,即Pari,if(a instanceof Pari)是错误的
又如:
Pari pari1 = (Pari) a
无论何时使用instanceod或者设计泛型类型的强制类型转换表达式都会看到一个编译器警告。
同样道理,getClass方法总是返回原始类型。
if (pari1.getClass() == pari2.getClass())返回true,因为两次getClass()都是返回Pari.class
- 不能实例化类型变量,不能使用像new(…),new t[…]或T.class这样的表达式中的类型变量。
- 类型参数命名约定
- E:表示集合元素(Element)、V:表示数值(Value)、L:表示键(key)、T:表示类型
- 可以通过super和extends实现下限和上限的限制
- 多界限泛型参数类型extends的第一个类型允许具体类(也可以接口),第二或更多参数类型必须是接口
private static void lowerBoundedWildcardsDemo(List<? extends Number> producer, List<? super Number> consumer) { // PECS stands for producer-extends, consumer-super. // 读取数据(生产者)使用 extends for (Number number : producer) { } // 操作输出(消费者)使用 super consumer.add(1); consumer.add((short) 2); }
@SuppressWarnings(“unchecked”) 可以抑制泛型在编译时的检查
Java函数式设计
匿名内置类基本特性:
- 无名称类、基于多态、允许多个抽象方法
- 声明位置(执行模块):
- static block
- 实例 block
- 方法
- 构造器
- 并非特殊的类结构
- 类全名称:
p
a
c
k
a
g
e
.
{package}.
- 类全名称:
函数式接口
@FunctionalInterface用于标记在接口上
标注该接口只有一个抽象方法(不包括default默认方法实现、Object的公开方法),如果任一接口满足以上函数式接口的要求,无论接口声明中是否标注,均能被编译器视作函数式接口
基本特性:
- 所有的函数式接口都引用一段执行代码
- 函数式接口没有固定的类型,固定模式(SCFP = Supplier + Consumer + Fucntion + Predicate)
Consumer函数接口有个方法andThen可以实现Fluent操作
Function函数接口也有andThen和compose方法
// consumer2 -> consumer -> consumer
consumer2.andThen(consumer).andThen(consumer).accept("Hello,小马哥");
Supplier 接口定义
-
基本特点:只出不进
-
编程范式:作为⽅法/构造参数、⽅法返回值
-
使⽤场景:数据来源,代码替代接⼝
当用函数式接口做返回值的时候,你可以理解为是一种延迟处理的对象,而处理的时机决定在你调用函数式接口中的方法时,也就是它不一定执行,当前不阻塞,事后去调用。
Consumer 接口设计
-
基本特点:只进不出
-
编程范式:作为⽅法/构造参数
-
使⽤场景:执⾏ Callback
Function<T,R> 接口设计
-
基本特点:有进有出
-
编程范式:作为⽅法/构造参数
-
使⽤场景:类型转换、业务处理等
Predicate 接口设计
-
基本特点:boolean 类型判断
-
编程范式:作为⽅法/构造参数
-
使⽤场景:过滤、对象⽐较等
函数式接口妙用:通过利用函数式对象的泛型使同个方法更灵活。又叫行为参数化
Collection<Integer> even = filter(numbers, num -> num % 2 == 0);
Collection<Integer> odd = filter(numbers, num -> num % 2 != 0);
private static <E> Collection<E> filter(Collection<E> source, Predicate<E> predicate) {
// 集合类操作,请不要直接利用参数
List<E> copy = new ArrayList<E>(source);
Iterator<E> iterator = copy.iterator();
while (iterator.hasNext()) {
E element = iterator.next();
if (!predicate.test(element)) {
iterator.remove();
}
}
return Collections.unmodifiableList(copy);
}
Java接口设计
通用设计 – 类/接口名
-
模式:(形容词)+ 名词
举例:
- 单名词:Java.lang.String
- 双名词:Java.util.ArrayList
- 形容词+名词:java.util.LinkedList
通用设计 – 可访问性,四种修饰符
- public
- (default):仅限当前package下访问
- protected:不能用于修饰最外层的class
- private:不能用于修饰最外层的class
通用设计 – 可继承性
- final:final不具备继承性,仅用于实现类,不能与 abstract 关键字同时修饰类
- 从 Java 1.5 开始,对象属性可以通过反射修改
- 举例:java.lang.String
- 非final:最常见/默认的设计手段,可继承性依赖于可访问性
- 举例:java.io.FileSystem
具体类设计 – 命名模式
- 前缀:“Default”、“Generic”、“Common”、“Basic”
- 后缀:“Impl”
常见场景
- 功能组件
- HashMap
- 接口/抽象类实现
- HashMap <- AbstractMap <- Map
- 数据对象
- POJO
- 工具辅助
- *Utils
- ViewHelper
- Helper
抽象类设计
常见场景
- 接口通用实现(模板模式)
- AbstractList
- AbstractSet
- AbstractMap
- 状态/行为继承
接口设计
常见模式:无状态、完全抽象(< Java8)、局部抽象(Java 8+)、单一抽象(Java 8 函数式接口)
- Serializable
- Cloneable
- AutoCloseable
- EventListener
从Java8开始,接口有default方法,是基于什么考虑?
方法兼容,函数式接口考虑(我们知道函数式接口只能有一个抽象方法)。
枚举设计
- 枚举(enum) 实际是 final class
- 枚举(enum) 成员修饰符为 public static final
values()
是 Java 编译器做的字节码提升
Java向上转型及内存分析
Java中的引用类型(reference type)
- Java中的数据类型分为两类:基本数据类型(int, double)和引用类型(String)。引用类型对象中保存着一个地址(称为“引用”),引用指向实际对象(称为“实例”),实际对象中保存着值。
- 内存分配
Heap中的对象会继续指向内存中的数据段(data segment)。数据保存对象的实际值
**向上转型 ** 定义:将父类的引用指向子类的实例 or 将子类对象赋值给父类引用
Dog dog = new Dog();
Animal anim = (Animal) dog; //实际对象类型没变,仅引用类型改变了
anim.eat();
由于实际对象类型没变,所以,anim调用的eat方法仍是Dog类中重写的eat方法,而不是父类Animal类中的eat方法。
内存分析
我们来分析以下转型代码在内存中的表示:
Dog dog = new Dog();
Animal anim = (Animal)dog;
CORS同源策略
CORS
全称是 Cross-Origin Resource Sharing
,直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。
- 域,指的是一个站点,由
protocal
、host
和port
三部分组成,其中host
可以是域名,也可以是ip
;port
如果没有指明,则是使用protocal
的默认端口 - 资源,是指一个
URL
对应的内容,可以是一张图片、一种字体、一段HTML
代码、一份JSON
数据等等任何形式的任何内容 - 同源策略,指的是为了防止
XSS
,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。
了解了这三个概念,我们就能理解为什么有 CORS
规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS
。
cookie作用域理解
- domain表示的是cookie所在的域,默认为请求的地址. 默认cookie的域是当前域名。
- 首先 一级域名为 www.a.com 和 a.com,二级域名为 xxx.a.com 二级域名下是可以写当前域名的cookis 也可以写父级域名下的cookie(www.a.com或a.com).
- 这里需要注意一点:为了保证安全性,不能把domain的值设置成非主域的域名,一定是同域之间的访问。
- 当我们想实现跨域共享cookie的时候,很容易会想到 在a.com 下写cookie进b.com下, 例如在a网站下 cookies.setDomain(b.com).这种方式是不行的,因为跨域是写不进b.com下的cookies
SpringBoot解决跨域问题
SpringBoot可以基于Cors解决跨域问题,Cors是一种机制,告诉我们的后台,哪边(origin )来的请求可以访问服务器的数据。
预检请求
在 CORS
中,定义了一种预检请求,即 preflight request
,当实际请求不是一个 简单请求
时,会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次 OPTIONS
请求,并带上下面三个 headers
:
Origin
:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个header
,服务器将不会进行CORS
验证。Access-Control-Request-Method
:值为实际请求将会使用的方法Access-Control-Request-Headers
:值为实际请求将会使用的header
集合
如果服务器端 CORS
验证失败,则会返回客户端错误,即 4xx
的状态码。
否则,将会请求成功,返回 200
的状态码,并带上下面这些 headers
:
Access-Control-Allow-Origin
:允许请求的域,多数情况下,就是预检请求中的Origin
的值Access-Control-Allow-Credentials
:一个布尔值,表示服务器是否允许使用cookies
Access-Control-Expose-Headers
:实际请求中可以出现在响应中的headers
集合Access-Control-Max-Age
:预检请求返回的规则可以被缓存的最长时间,超过这个时间,需要再次发起预检请求Access-Control-Allow-Methods
:实际请求中可以使用到的方法集合
浏览器会根据预检请求的响应,来决定是否发起实际请求。
Access-Control-Allow-Credentials响应头表示是否可以将对请求的响应暴露给页面。返回
true则可以,其他值均不可以。Credentials可以是
cookies, authorization headers或
TLS client certificates。如果被设置为true,服务器不得设置
Access-Control-Allow-Origin的值为
*,需要指定域名,否则当需要发送
Cookie` 到服务器时,浏览器响应时将会报错。
小结
到这里, 我们就知道了跨域请求会经历的故事:
- 访问另一个域的资源
- 有可能会发起一次预检请求(非简单请求,或超过了
Max-Age
) - 发起实际请求
工厂模式
简单工厂
实际上我们可以理解为是一种编程习惯, 将类似对象的初始化放下一个地方, 便于管理.
它提供了一个工厂(表妹), 来根据不同的指令(drinkType)来生产不同的饮料产品(橙汁, 可乐, 酸梅汤).
相对简单, 适用于要创建类似(实现同一接口的)的产品, 且产品种类不多, 扩展可能性不大的情况. 当需要增加一中饮料时, 我们需要修改工厂(表妹)的实现, 增加drinkType的对应实现.
常见的三种实现,其一为使用if语句、其二使用Enum枚举来实现、其三使用泛型+Class.forName实现
public <T extends Human> T creatHuman(Class<T> c) { // TODO Auto-generated method stub Human human = null; try{ human = (Human)Class.forName(c.getName()).newInstance(); }catch(Exception e){ System.out.println("人类生产错误"); e.printStackTrace(); } return (T)human; }
工厂方法
顾名思义, 有一个工厂, 工厂(饮料机)里有那么一个方法(定义了一个创建对象的接口makeDrink), 可以生产产品(Drink). 由实现了这个工厂方法的类来决定具体生产出什么产品(可以是可乐, 橙汁, 奶茶等).
相比于简单工厂, 工厂方法有良好的扩展性, 当我们需要增加一种饮料时, 不需要去修改工厂, 只需扩展一个新的工厂, 实现其工厂方法, 提供新的饮料即可.
这实际上就是典型的, 通过继承/实现, 来达成了对修改关闭, 对扩展开放的效果.
另外, 从简单工厂到工厂方法, 我们也可以理解为是一次Switch Statements的重构.
对于”Switch Statements的重构”, 有兴趣的同学可以参看<<重构–改善既有代码的设计>>一书的3.10节. 那是一本好书, 2010年的时候华为的一位技术经理推荐给我的, 感谢他.
另外, 并不是我们以后遇到Switch就要想着改造, 遇到简单工厂就想着用工厂方法…还需根据实际情况取用合适的.
抽象工厂
同样, 从名字中, 我们大致能了解, 抽象工厂描述的一个抽象的工厂, 其可以生产一系列的相关的或是互相依赖的产品.
抽象工厂和工厂方法有很多类似之处, 都是创建产品, 都是通过继承/实现, 来达成了对修改关闭, 对扩展开放的效果.
然而, 抽象工厂相较于工厂方法, 它的重点, 是它解决的是一个产品族(相关的, 或是互相依赖的产品们)的创建问题, 而非仅仅是一类产品.
以本故事来说, 工厂方法是用来创建一类产品, 通过他创建出来的都是饮料. 而抽象工厂是用来创建一系列产品, 包括店铺, 收银台, 餐具等, 这些产品是相关的, 都是一个分店所需要的.
打个比方, 如果我有一个轮胎工厂, 我生产的东西都是轮胎, 只是规格不同, 我就可以使用工厂方法; 如果我是一个汽车工厂, 我生产汽车, 它需要轮胎, 车架, 发动机… 那么我就应用使用抽象工厂.
原型模式(prototype)
原型模式是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象
原型模式是一种创建型设计模式,允许一个对象在创建另一个可定制的对象,无需知道创建的细节
工作原理是:通过原型对象拷贝它们自己来实现创建,即对象.clone()
这里牵涉到深clone和浅clone
浅拷贝的介绍
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值赋值一份给新的对象
- 但对于数据类型是引用类型的成员变量,比如说数组,类对象等,那么浅拷贝会进行引用的副本传递,(Java中不存在引用传递),也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。在这种情况下,修改一个对象中的成员变量会影响到另一个对象的该成员变量。
- 浅拷贝实现Cloneable接口,使用默认的clone方法来实现
深拷贝的介绍
- 复制对象的所有基本数据类型的成员变量值
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,也就是深拷贝是对整个对象(包括对象的引用类型)进行拷贝
- 深拷贝实现方式有两种:
- 重写clone方法实现深拷贝
- 通过对象序列化实现深拷贝(推荐)
需要注意地方
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能提高效率
- 不用重新初始化对象,而是动态获得对象运行时的状态
- 如果原始对象发生变化,其他克隆对象也会发生变化,无需修改代码
- 缺点:需要为每一个类写一个克隆方法,这对全新的类来说不难,但对已有的类进行改造,需要改动源代码,违背了OCP(开闭)原则。
建造者模式
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
比如一台电脑包括主机、显示器、键盘等外设,这些部件组成了完整的一台电脑。如何将这些部件组装成一台完整的电脑并返回给用户,这是建造者模式需要解决的问题。建造者模式(builder)又称为生成器模式,从名词就可以看出,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。
上图中包含了建造者模式的四个主要角色:
- Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象方法,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
- ConcreteBuilder(具体建造者):它实现了Builder抽象方法,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。依赖于Product。
- Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
- Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
与工厂模式区别:
建造者模式的优点是封装性好,且易于扩展。上面提到,对于客户端而言,只需关心具体的建造者即可。使用建造者模式可以优先的封装变化,product和builder比较稳定,主要的业务逻辑封装在控制类中对整体可取得比较好的稳定性。如需扩展,只需要加一个新的建造者,对之前代码没有影响。
与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——指挥者类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给指挥者类。由指挥者类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。
适配器模式
基本介绍
-
适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)
-
适配器模式属于结构型模式
- 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
在java中使用流时为什么总是使用1024大小的byte数组?
现代操作系统的内存管理都具有分页机制,而内存页的大小都是1024的整数倍,定义1024整数倍大小的缓冲区在分配内存时将不会形成碎片。
什么是字节码提升
字节码提升是通过字节码操作,修改类的定义,达到辅助类一些操作,AOP。深入Java虚拟机关于ClassLoader部分
JDK1.9以后String类的实现,从char[] 数组转成 byte[] 数组
使用byte数组可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。
对单个字符的操作使用Character类
ReferenceQueue 引用队列
ReferenceQueue 引用其实也可以归纳为引用中的一员,可以和三种引用类型组合使用【软引用、弱引用、虚引用】。 在创建Reference时,手动将Queue注册到Reference中,而当该Reference所引用的对象被垃圾收集器回收时,JVM会将该Reference放到该队列中,而我们便可以对该队列做些其他业务,相当于一种通知机制。
对比Exception和Error,另外,运行时异常与一般异常有什么区别?
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception和Error体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常 见的比如OutOfMemoryError之类,都是Error的子类。
Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面我介绍的不可查 的Error,是Throwable不是Exception。
不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕 获,并不会在编译期强制要求
面向对象的基本要素:封装、继承、多态。
- 封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致 的难缠bug太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。
- 继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度 滥用继承,可能会起到反效果。
- 多态,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的 参数,本质上这些方法签名是不一样的。
类加载机制
1.类的生命周期
如上图所示,描述了类的生命周期。其中加载、验证、准备、初始化、卸载这五个动作是存在先后顺序的,而解析阶段有可能在初始化之后完成的。
这里着重说一下准备阶段【重点】
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
内存分配的对象:要明白首先要知道Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始。
举个例子:例如下面的代码在准备阶段,只会为 A属性分配内存,而不会为 C属性分配内存。
public static int A = 666;
public static final int B = 666;
public String C = "jvm";
初始化的类型:在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的默认值,而不是用户代码里初始化的值。 但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如B在准备阶段之后,B的值将是 666,而不再会是 0。
之所以 static final 会直接被复制,而 static 变量会被赋予java语言类型的默认值。其实我们稍微思考一下就能想明白了:
A和B两个的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在 Java 中代表不可改变的意思,意思就是说 B的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。
2.双亲委派模型
介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在jvm中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。 从jvm角度来看只存在两种类加载器
- 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载JAVA_HOME/lib/目录中的,或者被-Xbootclasspath参数所指定的路径中并且被虚拟机识别的类库。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader:
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。下面举一个大家都知道的例子说明为什么要使用双亲委派模型。
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
进程和线程、并发和并行
-
进程是资源(cpu、内存等)分配的基本单位,它是程序执行时的一个实例
-
线程是程序执行时的最小单位,一个进程可以有多个线程,线程间共享进程的所有资源,每个线程还有自己的堆栈和局部变量。
-
并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。
-
并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。
Java的三个包JUC(java.util.concurrent)、java.util.concurrent.atomic、java.util.concurrent.locks
怎么理解阻塞非阻塞与同步异步的区别?
-
同步和异步
它们关注的是消息通信机制,所谓同步就是在发出一个调用时,在没有得到结果前该调用就不返回。异步则相反,调用发出后,这个调用就直接返回了,也就是说调用者不会立即得到结果,而是事后被调用者通过状态或通知、回调函数来告诉通知者。
例如:你打电话问书店老板有没有”xxx”书,老板说要你等一下,他去找,就一直找啊一直找,假设找了50s,这50s都没有挂电话,这就是同步。异步就是你打电话过去,他说找好了在告诉你,就挂了电话,找到后打电话告诉你就是一个通知或回调的方法。
synchronized and lock
两者的区别如下:
ReentrantLock是juc中一个非常有用的组件,很多并发集合类是用它实现的,例如ConcurrentHashMap。它具有是哪个特性:等待可中断,可实现公平锁,以及锁可以绑定多个条件。
ReentrantLock和synchronized关键字一样,属于互斥锁,但synchronized的锁是非公平的。
公平锁指多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造方法使用公平锁,用lock获得锁,unlock释放锁,但它需要将方法置于try-finally块中,以免忘记释放锁。
性能上,在1.6以前,ReentrantLock明显优于synchronized,但1.6以后加入了很多针对锁的优化,所以两者性能基本持平。
在使用lock的时候,可以抛弃Object.wait和notify的写法,通过lock的newCondition使用Condition接口。
Condition的功能类似于传统线程技术中的Object.wait()和Object.notify()方法的功能,但它是将这些方法分解成不同的对象,所以可以将这些对象与任意的Lock实现组合使用,实现在不同的条件下阻塞或唤醒线程;也就是说,这其中的Lock替代了synchronized方法和语句的使用,Condition替代了Object 监视器方法(wait、notify 和 notifyAll)的使用。
interrupt(),interrupted() 和 isInterrupted() 的区别
interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。
interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
isInterrupted():获取调用该方法的对象所表示的线程,不会清除线程的状态标记。是一个实例方法。
synchronized的基本规则如下:
我们将synchronized的基本规则总结为下面3条
**第一条:**当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
**第二条:**当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
**第三条:**当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
注意:静态同步方法锁的是类,普通同步方法锁的是对象,两者之间不冲突,没有竞态条件
常见的锁机制
重量级锁
我们知道,我们要进入一个同步、线程安全的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会马上进入阻塞的状态,等待那个持有锁的线程释放锁,然后再把我们从阻塞的状态唤醒,我们再去获取这个方法的锁。
这种获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。
自旋锁
我们知道,线程从运行态进入阻塞态这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到用户态到内核态的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。
刚才我说线程拿不到锁,就会马上进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历阻塞/唤醒这个花时间的过程了。
然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 — 自旋锁。
自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做空循环,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。
轻量级锁
上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。
之所以要加锁,是因为他们害怕自己在这个方法执行的时候,被别人偷偷进来了,所以只能加锁,防止其他线程进来。这就相当于,每次离开自己的房间,都要锁上门,人回来了再把锁解开。
这实在是太麻烦了,如果根本就没有线程来和他们竞争锁,那他们不是白白上锁了?要知道,加锁这个过程是需要操作系统这个大佬来帮忙的,是很消耗时间的,。为了解决这种动不动就加锁带来的开销,轻量级锁出现了。
轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。
悲观锁和乐观锁
最开始我们说的三种锁,重量级锁、自旋锁和自适应自旋锁,进入方法之前,就一定要先加一个锁,这种我们为称之为悲观锁。悲观锁总认为,如果不事先加锁的话,就会出事,这种想法确实悲观了点,这估计就是悲观锁的来源了。
而乐观锁却相反,认为不加锁也没事,我们可以先不加锁,如果出现了冲突,我们在想办法解决,例如 CAS 机制,上面说的轻量级锁,就是乐观锁的。不会马上加锁,而是等待真的出现了冲突,在想办法解决。
ThreadLocal解读
ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以避免资源竞争带来的多线程的问题,这种解决多线程的安全问题和lock(这里的lock 指通过synchronized 或者Lock 等实现的锁) 是有本质的区别的:
- lock 的资源是多个线程共享的,所以访问的时候需要加锁。
- ThreadLocal 是每个线程都有一个副本,是不需要加锁的。
- lock 是通过时间换空间的做法。
- ThreadLocal 是典型的通过空间换时间的做法。
当然他们的使用场景也是不同的,关键看你的资源是需要多线程之间共享的还是单线程内部共享的
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/15220.html