数据类型
- 基本数据类型 & 包装类型
在Java语言中,new
一个对象是存储在堆里的,使用时,通过栈中的引用来找到这些对象;
对于经常用到的类型,如 int 等,如果我们每次使用这种变量的时候都需要new一个对象的话,就会比较笨重。所以,和C++一样,Java提供了基本数据类型,这种数据的变量不需要使用new创建,它不会在堆上创建,而是直接在栈内存中存储,因此会更加高效;
在使用数值型的基础数据类型时,当值既定范围的时候,不会报错也不会抛出异常,所以需要根据业务逻辑做好相关判断;
与8中基础类型相对应的,有八个包装类:
基本数据类型 | 包装类 |
---|---|
byte | Byte |
boolean | Boolean |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
提供了八种基本数据类型,为什么还要提供包装类呢?
因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double等类型放进去的;
- 自动拆箱 & 自动装箱
在 Java SE5 中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能;
自动装箱: 就是将基本数据类型自动转换成对应的包装类;
自动拆箱:就是将包装类自动转换成对应的基本数据类型;
自动装箱都是通过包装类的 valueOf()
方法来实现的.自动拆箱都是通过包装类对象的 xxxValue()
来实现的;
那些地方会自动装箱拆箱?
基本数据类型放入集合:自动装箱;
包装类型和基本数据类型进行比较:自动拆箱;
含有包装类型的运算:自动拆箱;
三目运算符第二位和第三位操作数分别是基础类型和包装类型时: 自动拆箱;(拆箱时遇到第二个操作数是null就会报错)
函数参数与返回值:装箱或拆箱以返回值为准;
在开发中我们应该避免自动拆装箱。
- String
字符串一旦创建就无法修改;
如果需要一个可修改的字符串,应该使用StringBuffer或者StringBuilder,因为每次修改都有新的string对象被创建出来,会占用大量内存,且浪费GC时间;
- String池
实际开发中我们常用字面量来初始化字符串;在JVM中,为了减少相同的字符串的重复创建,节省内存,会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池;
几个关键字
- transient
被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。
- instanceof
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
- synchronized
被synchronized
修饰的代码块及方法,在同一时间,只能被单个线程访问,是一种重量级锁(基本原理);
- volatile
用来解决并发安全问题,配合synchronized使用(基本原理);
final
final是Java中的一个关键字,它所表示的是“这部分是无法修改的”;
- static
static表示“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块;
- const
const是Java预留关键字,用于后期扩展用,用法跟final相似,不常用
集合
1. Set和List
前者元素无序且不可重复,后者恰好相反;
set如何保证元素不重复:
a. TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入null值,底层是TreeMap的keySet(),而TreeMap是基于红黑树实现的,红黑树是一种平衡二叉查找树,它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。
b. HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null值,但只能放入一个null;HashSet底层实际上就是一个HashMap
2. ArrayList、LinkedList与Vector
ArrayList实际是一个数组,其大小将会动态地增长,增速是50%;
LinkedList 是一个双向链表,在添加和删除元素时具有比ArrayList更好的性能,但在随机存取方面弱于ArrayList;
LinkedList 还实现了 Queue 接口;
Vector 和ArrayList类似,但属于强同步类,增速是100%;
3. HashMap
4. CopyOnWriteArrayList
读写分离的列表,读不加锁,但是写会被加锁,所以是线程安全的;
写的过程是:先把原来的数组copy出来,实际操作的是新数组,操作完之后再将原数组的引用指向新数组;
多用于多读少写的场景;
5. 集合的迭代方式
- 通过普通for循环迭代
- 通过增强for循环迭代(实际是使用Iterator)
- 使用Iterator迭代
- 使用Stream迭代(线程安全,修改操作不会影响数据源)
6.fail-fast
在遍历迭代时,修改集合的结构,就可能报错(ConcurrentModificationException);
一般出现在:使用Iterator时,在循环体修改了集合的结构(add,remove);
之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改!
使用listIterator就可以不受其影响;
值传递
- 值传递和引用传递
值传递(pass by value)是指在调用函数时将实际参数复制
一份传递到函数中,这样在函数中如果对参数
进行修改,将不会影响到实际参数;
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接
传递到函数中,那么在函数中对参数
所进行的修改,将影响到实际参数;
引用传递中,实际也是将对象的引用复制一份,然后进行“值传递”;
深拷贝和浅拷贝
不管是浅拷⻉还是深拷⻉,都可以通过调⽤ Object 类的 clone() ⽅法来完成;
拷贝都会生成新的对象;
- 浅拷贝
public class 浅拷贝 implements Cloneable { private final Date birthday; private final String name; private final int age; @Override public String toString() { return "(@"+System.identityHashCode(this)+"){" + "birthday=(@"+System.identityHashCode(birthday)+")'" + birthday.toString() + '\'' + ",name=(@"+System.identityHashCode(name)+")'" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws Exception { 浅拷贝 test1 = new 浅拷贝(new Date(), "张三", 10); 浅拷贝 test2 = (浅拷贝) test1.clone(); System.out.println("原始对象:"); System.out.println(test1); System.out.println("浅拷贝后的对象:"); System.out.println(test2); test2.getBirthday().setTime(System.currentTimeMillis() - 60_000L); System.out.println("修改后的原始对象:"); System.out.println(test1); System.out.println("修改后的浅拷贝后对象:"); System.out.println(test2); } }
输出结果为:
原始对象:
(@1705736037){birthday=(@455659002)'Tue Aug 03 15:45:20 CST 2021',name=(@2003749087)'张三', age=10}
浅拷贝后的对象:
(@1324119927){birthday=(@455659002)'Tue Aug 03 15:45:20 CST 2021',name=(@2003749087)'张三', age=10}
修改后的原始对象:
(@1705736037){birthday=(@455659002)'Tue Aug 03 15:44:20 CST 2021',name=(@2003749087)'张三', age=10}
修改后的浅拷贝后对象:
(@1324119927){birthday=(@455659002)'Tue Aug 03 15:44:20 CST 2021',name=(@2003749087)'张三', age=10}
可以看出,浅拷贝对于复杂对象的成员,只是对其引用进行拷贝,并不会重新分配一个新的对象指向它。 很明显,这样会有一个问题,那就是复制出来的对象,可能会影响到原始对象,这央视很危险的,于是我们就需要深拷贝,它能复制出一个完全不相关的对象来。
- 深拷贝
对于字段较少的对象,我们可以通过重写clone方法来实现深拷贝:
protected Object clone() throws CloneNotSupportedException { 深拷贝 tester = (深拷贝) super.clone(); tester.setBirthday((Date) tester.getBirthday().clone()); return tester; }
只需要将代码做如上所示的改动,便会得到不一样的结果:
原始对象:
(@114132791){birthday=(@586617651)'Tue Aug 03 16:15:24 CST 2021',name=(@1018937824)'张三', age=10}
浅拷贝后的对象:
(@905654280){birthday=(@1915058446)'Tue Aug 03 16:15:24 CST 2021',name=(@1018937824)'张三', age=10}
修改后的原始对象:
(@114132791){birthday=(@586617651)'Tue Aug 03 16:15:24 CST 2021',name=(@1018937824)'张三', age=10}
修改后的浅拷贝后对象:
(@905654280){birthday=(@1915058446)'Tue Aug 03 16:14:24 CST 2021',name=(@1018937824)'张三', age=10}
以上这种方式只适用于字段较少的情况,如果字段较多,代码写起来却是会令人头疼,我们有另外更好的办法:序列化 :
public Object deepClone() throws IOException, ClassNotFoundException { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); }
调用deepClone()方法代替clone()方法,就能实现深拷贝。