什么是面向对象
简单来说就是讲一类具有相似特征的抽象化成一个整体,从更高层次来进行系统建模,更贴近事物的自然运行模式,例如:人、动物、运动员等都属于一个抽象化出来的对象
面向对象的三大基本特征和五大基本原则
三大基本特征
- 封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承
子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容
- 多态
指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果
五大基本原则
- 单一职责原则SRP(Single Responsibility Principle)
指一个类的功能要单一,不能包罗万象。如一个水泥工,不能让他和水泥又让他去做电工的事
- 开放封闭原则OCP(Open Close Principle)
指一个模块在扩展性方面应该是开放的而在更改性方面是封闭的。如一个网络模块,原来只做服务端功能,现在要加入客户端功能,那么应该在不修改服务端代码前提下就能增加客户端功能的实现。
- 替换原则LSP(Liskov Substitution Principle)
子类可以替换父类并出现在父类能够出现的任何地方。如一个电工师傅和电工徒弟,徒弟继承了师傅的所有技能,有一家老顾客让师傅去修电器,师傅没空,那么叫徒弟修也是一样能实现的
- 依赖倒置原则DIP(Dependency Inversion Principle)
高层次的模块不应该依赖低层次的模块,他们都应该依赖于抽象
抽象不应该依赖于具体实现,具体实现应该依赖于抽象
- 接口分离原则ISP(Interface Segregation Principle)
模块之间要通过接口隔离开,而不是通过具体的类强耦合
字符串(String)
字符串的不可变性
不可变实现原理
- 类使用final关键字修饰,标识类不可被继承,保证了外部不能直接重写方法的实现
- 使用
private final char value[]
修饰,private避免了外部直接修改value值,final保证了value的指向地址不变 - 对原有字符串的修改都会返回一个新的String对象
好处
- 字符串池的实现,只有字符串是不可变的,才能却表相同字符串再堆中指向的地址相同
- 提高安全性
数据库链接或者socket编程中,主机名和端口都是字符串形式传输,由于不可变,所有他的值不能被修改,否则改变其指向的值会造成安全漏洞;类加载机制的安全性,字符串的不可变性确保了正确的类被加载
JDK 6 和 JDK 8 substring 的原理及区别
//jdk6源码
public String(int offset, int charCount, char[] chars) {
this.value = chars;
this.offset = offset;
this.count = charCount;
}
public String substring(int start, int end) {
if (start == 0 && end == count) {
return this;
}
// Fast range check.
if (start >= 0 && start <= end && end <= count) {
return new String(offset + start, end - start, value);
}
throw startEndAndLength(start, end);
}
//jdk8源码
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {·
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
总结:
-
jdk6:String维护了三个成员变量
char value[]:真实值,int offset:第一个的索引位置,int count:字符长度
,调用substring
方法时会创建一个新的String对象,但是值引用依旧指向原先的String,这新str与原str只有 offset和count不同,
当你有一个很长的字符串却只需要去其中很小一段时会引用整个str导致性能问题,而且也会导致引用一直存在而无法回收发生内存泄漏 -
jdk7:调用
substring
方法时会在堆内存中创建一个新的数组
字符拼接的几种方式和区别
- String类重载’+'运算符
使用"+"处理对象时,会默认创建一个StringBuild,并调用append方法
- String类方法
a.concat(b)
//concat源码
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {//如果拼接长度
return this;
}
int len = value.length;//获取当前数组长度
char buf[] = Arrays.copyOf(value, len + otherLen);//复制一个数组,长度为原始字符长度+拼接字符长度
str.getChars(buf, len);
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
/**
* public static void arraycopy(Object src,
* int srcPos,
* Object dest,
* int destPos,
* int length)
* 将指定源数组中的数组从指定位置复制到目标数组的指定位置
* src - 源数组。
* srcPos - 源数组中的起始位置。
* dest - 目标数组。
* destPos - 目标数组中的起始位置。
* length - 要复制的数组元素的数量。
*/
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
replaceFirst、replaceAll、replace 区别
//replace源码
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {//找到相等的字符串的的索引
break;
}
}
if (i < len) {
char buf[] = new char[len];//创建新的数组,成都为原始字符串的长度
//将未能相等字符索引前的所有字符添加到新数组中
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
//替换掉当前索引以及索引后的字符
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
- replaceFirst:基于正则表达式,替换掉匹配到的第一个字符
- replaceAll:基于正则表达式,替换掉匹配到的所有字符
若replaceAll()和replaceFirst()所用的参数据不是基于规则表达式,则效果与replace()一样
String.valueOf 和 Integer.toString 的区别
参数为int类型时,实际上调用Integer.toString方法
// String.valueOf源码
public static String valueOf(int i) {
return Integer.toString(i);
}
//Integer.toString源码
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
switch 对 String 的支持
通过hashCode找到对应的case:值(hash),在里面嵌套通过equals比较字符串是否相等
@Test
public void testString(){
String str = "abc";
switch (str){
case "abc":
System.out.println(str);
break;
case "bcd":
System.out.println(str+"sss");
break;
default:
System.out.println(1);
}
}
字符串池、运行时常量池、Class 常量池、intern
字符串池(string pool)
在类加载完成后,经过验证、准备阶段后在堆中生成的字符串对象实例,然后将该字符串对象实例引用值存到
string pool
中,在HotSpot VM里实现的string pool
功能的是一个StringTable类,他是一个hash表,里面存的是驻留字符串(即双引号括起来)的引用(而不是字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了驻留字符串
的身份。这个StringTable在每个HotSpot VM中只有一份,被所有类共享
class常量池
当java文件编译成class文件后,会生成class常量池, class文件包含类的版本、字段、方法、接口等描述信息,还有一项就是常量池,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
- 字面量:文本字符串、被声明的final常量、基本数据类型值
- 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
运行时常量池
类加载到内存后,将class常量池中的内容存放到运行时常量池中,而经过解析后,会将常量池中的符号引用替换为直接引用,解析过程会查询StringTable,保证运行时常量池所引用的字符串与全局字符串池找那个所引用保持一致
intern方法
返回字符串对象的规范化表示形式,一个初始时为空的字符串池(string pool),它由类 String 私有地维护。当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串(地址)。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t)为 true 时,s.intern() == t.intern() 才为 true。所有字面值字符串和字符串赋值常量表达式都是内部的。
public static void main(String[] args){
String a = "a";
String b = "b";
String c = "ab";
String d = a + b;
String e = new String("ab");
String f = "a"+"b";
System.out.println(c == d);//比较c和d指向常量池的地址,由于只有静态变量才会加入到字符串池中,而变量不会,所以d是一个新对象 故为fasle
System.out.println(c == e);//比较的是c的字符地址和e在堆中实例的地址 所有为false
System.out.println(c == f);//比较的是c的字符地址和f的字符地址的地址 所有为true
System.out.println(d == e);//比较的是d的地址和e在堆中实例的地址 所有为false
System.out.println(d == f);//比较的是d的地址和f的字符地址的地址 所有为false
System.out.println(e == f);//比较的是f的字符地址和e在堆中实例的地址 所有为false
System.out.println("===================");
System.out.println(e.intern() == c);// 比较的是f的字符地址和c字符地址 所有为true
System.out.println(e.intern() == d);//false
System.out.println(e.intern() == f);//true
}
常用关键字
transient
作用于变量上,防止属性被序列化
- 一旦变量被transient修饰,变量将不再是持久化的一部分,该变量内容在序列化后无法获得访问
- 用户自定义类需要实现Serializable接口才能使用transient修饰变量
- 只能修饰类的成员变量
- 一个静态变量不管是否被transient修饰,都不能被序列化
- 若实现Externalizable接口,则没有任何东西可以自动序列化,需要重写writeExternal方法,在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关
volatile
一旦一个共享变量(类成员变量,类静态变量)被volatile修饰之后,那么就具备了两层语义
public class TestVolatile {
public volatile int count = 0;
public static void main(String...ages){
TestVolatile test = new TestVolatile();
for(int i = 0;i < 10;i++){
new Thread(() -> {
for(int j = 0;j < 1000;j++){
test.count++
}
})
}
}
}
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某变量的值,这新值对其他线程来说是立即可见的。因为值会被强制立即写入主存
- 禁止指令重排序
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
- 不保证操作的原子性
自增操作的三个子操作(它包括读取变量的原始值、进行加1操作、写入工作内存)可能会分割开执行
假如某个时刻变量inc的值为100,
一、线程1对变量进行自增操作,线程1先读取了变量inc的原始值100,然后线程1被阻塞了;二、然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时100,然后进行加1操作,并把101写入工作内存,最后写入主存。这样两个线程执行的都是100+1
synchronized
可用来给对象和方法或者代码块加锁
- 同步方法
给当前实例对象加锁,在同一个类的同一个实例对象中,访问此方法会锁住当前实例中所有的同步方法
public synchronized void test(){}
- 同步代码块
public void test(){
syschronized(this){
}
}
- 静态同步方法
给当前class对象加锁 访问此方法时会锁住当前类下所有的静态同步方法,任何实例访问都需等待当前锁释放
public synchronized static void test(){
}