Java se
一些需要注意的点
ConcurrentHashMap
CAS是乐观锁
CopyOnWriteArrayList的add方法
Lamda表达式
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量
对于functional interface(函数式接口)可以使用lamda表达式
- 任何接口,如果只包含一个抽象方法,就是一个函数式接口
Hello hello = new Hello() { @Override public void sayHello() { System.out.println("hello,happy"); } }; hello.sayHello(); Hello helloLamda = ()-> { System.out.println("hello,happy3"); }; helloLamda.sayHello();
关于::的部分
静态方法引用/(非静态方法引用)/(构造方法引用)
- 方法为静态方法
- 其为已经实现了的接口方法逻辑的方法,即返回值,参数类型已经个数与接口方法一致
- 接口 对象 = 该方法的类::该方法名 /(接口 对象 = 该方法对应的对象::该方法名) /(接口 对象 = 该方法的类::new)
特殊引用
- 如果参数为一个对象,且返回值仅仅是调用该对象的方法的返回值
- Person为x对象对应的类
简单了解java类加载的运行顺序
进入main方法的类时的操作
- 以main方法的类作为入口
- 给本类static修饰的成员变量赋一个默认值,如果还被final修饰,就赋值为定义的那个值,执行本类的static代码块(先执行static代码块还是成员变量由在代码中的位置决定)
- 其次是static方法
当使用 类的static属性 或者static方法 或者new一个对象时会触发类加载
当new一个时,如果父类还没有被初始化,会先对其父类先初始化
如果已经初始化了,会先执行父类的构造器
构造器初始化:
成员变量的初始化会在每一次new对象(调用方法)时发生,但构造器只会初始化一次,也就是说对于已经完成初始化的类来说,在次new对象的时候,不会在执行父类构造器,然而类成员还会在次初始化.
在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量的定义散部于方法或者构造器之间,仍然会在任何方法或者构造器被调用之前得到初始化.对象的引用也算变量的定义
final
泛型
泛型只存在于编码阶段,在运行阶段会被擦除,目的是为了兼容老版本的jdk
泛型的通配符为“?”,泛型的通配符使用的上边界以及下边界问题
extends 上边界 当需要读取,但不需要写入
super 下边界 当需要写入,但不需要读写
自定义一个泛型类
public class People<T> { private T name; public T getName() { return name; } public void setName(T name) { this.name = name; } }
public class People2<K,V> { private K name; private V age; public K getName() { return name; } public void setName(K name) { this.name = name; } public V getAge() { return age; } public void setAge(V age) { this.age = age; } }
构建这个泛型的对象
其中可以指定为任意类型
People<String> people = new People<>(); people.setName("happy");
People2<String, Integer> people2 = new People2<>(); people2.setName("happy"); people2.setAge(12);
定义一个泛型方法以及应用
静态的泛型方法需要自己声名占位符,与类的占位符无关,同理,普通方法也可以自定义
public class People3<T>{ public void show(T name){ System.out.println(name); } public T show2(T name){ return name; } public <M> M show3(M name){ return name; } public static <E>void showStatic(E name){ System.out.println(name); } public static <V> V showStatic2(V name){ return name; } }
People3<String> people3 = new People3<>(); people3.show("11111"); System.out.println(people3.show2("返回值的普通泛型方法")); People3.showStatic("happyStatic"); System.out.println(People3.showStatic2(111));
定义泛型接口
1:其实现类给接口指定类型
2:实现类也声明一个泛型
注解与反射
注解
@Target({ElementType.TYPE,ElementType.METHOD})//表示注解的作用域
@Retention(RetentionPolicy.RUNTIME)//什么时候生效
@Documented//表示方法已经过时,但是还能用
@Inherited//表示子类可以继承
public @interface MyAnnotation {
String value() default “”;
}
反射
一个类只有一个class对象
类被加载后,类的整个结构都被封装在class对象中
获取反射对象
-
通过路径
Class<?> c1 = Class.forName("com.smu.User");
- 通过对象获取
Class<? extends User> aClass = user.getClass();
- 通过类名获取
Class<User> userClass = User.class;
- 基本内置对象的封装类都有的TYPE属性
Class<Integer> type = Integer.TYPE;
通过反射获取类的信息
获取类名
- String name = c1.getName();
- String simpleName = c1.getSimpleName();
获取字段(带有Declared的是获取包括private的字段)
- Field[] fields = c1.getFields();
- Field username = c1.getField(“username”);
- Field[] declaredFields = c1.getDeclaredFields();
- Field username1 = c1.getDeclaredField(“username”);
获取方法(不带Declared是获取本类以及父类的public方法,带有Declared的是获得本类的所有包括private的方法)
- Method[] methods = c1.getMethods();
- Method getAge = c1.getMethod(“getAge”);
- Method[] declaredMethods = c1.getDeclaredMethods();
- Method setPassword = c1.getDeclaredMethod(“setPassword”, String.class);
获取构造器
- Constructor<?>[] constructors = c1.getConstructors();
- Constructor<?>[] declaredConstructors = c1.getDeclaredConstructors();
- Constructor<?> declaredConstructor1 = c1.getDeclaredConstructor(String.class, String.class, Integer.class);
获取对象
通过class对象获取(需要有无参构造器,需要是public修饰的),通过构造器获取,如果是private修饰的需要setAccessible(true);
- User o = (User) c1.newInstance();
- o.setAge(12);
- System.out.println(“class对象获取”+o);
- Constructor<?> declaredConstructor = c1.getDeclaredConstructor(String.class, String.class, Integer.class);
- declaredConstructor.setAccessible(true);
- User happy = (User) declaredConstructor.newInstance(“happy”, “889886”, 21);
- System.out.println(“通过构造器获取”+happy);
执行方法
可以通过类对象调用私有方法,也可以直接通过class对象获取方法
- Constructor<?> declaredConstructor = c1.getDeclaredConstructor(String.class, String.class, Integer.class);
- declaredConstructor.setAccessible(true);
- Object happy1 = declaredConstructor.newInstance(“happy”, “889886”, 15);
- Method setUsername = c1.getDeclaredMethod(“setUsername”, String.class);
- setUsername.invoke(happy1, “sjjshj”);
多线程
生产者消费者问题
- 等待:判断是否需要等待(this.wait())
- 业务
- 通知:通知其他线程(this.notifyAll())
虚假唤醒
怎么解决虚假唤醒?
直接上代码:主要修改了 1、if判断为while判断 2、notify 为notifyAll
解释:
while是为了再一次循环判断刚刚争抢到锁的线程是否满足继续执行下去的条件,条件通过才可以继续执行下去,不通过的线程只能再次进入wait状态,由其他活着的、就绪状态的线程进行争抢锁。
notifyAll主要是解决线程死锁的情况,每次执行完++或者–操作,都会唤醒其他所有线程为活着的、就绪的、随时可争抢的状态。
三种创建方式
- 继承Thread(Thread实现了Runnable接口)
- 实现Runnable(这种方式需要将该实现类作为参数调用Thread对象)
- 实现Callable
线程的状态
线程方法
yield()方法会重新让线程回到就绪状态,所以不一定会礼让成功
join()一定会礼让成功
线程状态
synchronized默认锁的对象是this
CopyOnWriteArrayList<?>()是线程安全的List
死锁的产生条件
什么时候会释放锁
由于等待一个锁定线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:
1、当前线程的同步方法、代码块执行结束的时候释放
2、当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
3、当前线程出现未处理的error或者exception导致异常结束的时候释放
4、调用obj.wait()会立即释放锁,当前线程暂停,释放锁,以便其他线程可以执行obj.notify(),但是notify()不会立刻立刻释放sycronized(obj)中的obj锁,必须要等notify()所在线程执行完synchronized(obj)块中的所有代码才会释放这把锁。而 yield(),sleep()不会释放锁。
除了以上情况外,只要持有锁的此案吃还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁:
1. 在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
2. 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
3. 在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃
关于Lock锁
每个对象都有锁,关于Object提供的几个方法
线程池
使用方式
具体实现方式1
具体实现方式2
volatile 与 synchronized 的比较:
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
线程安全性包括两个方面,①可见性。②原子性。
仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
JUC并发编程
并发三大特性
原子性、可见性、有序性
1. 原子性
含义
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作。
重要
不采取任何的原子性保障措施的自增操作并不是原子性的。
如何保证原子性
- 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
- 通过 Lock 接口保障原子性。
- 通过 Atomic 类型保障原子性。
2. 可见性
含义
当一个线程修改了共享变量的值,其他线程能够看到修改的值。
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
volatile 变量和普通变量区别
普通变量与 volatile 变量的区别是 volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
如何保证可见性
- 通过 volatile 关键字标记内存屏障保证可见性。
- 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
- 通过 Lock 接口保障可见性。
- 通过 Atomic 类型保障可见性。
- 通过 final 关键字保障可见性
3. 有序性
含义
即程序执行的顺序按照代码的先后顺序执行。
JVM 存在指令重排,所以存在有序性问题。
如何保证有序性
- 通过 synchronized关键字 定义同步代码块或者同步方法保障可见性。
- 通过 Lock接口 保障可见性
wait()和sleep()的区别
wait()
- 方法来自于Object
- 会释放锁
- 必须在同步代码块中使用
sleep()
- 方法来自于Thread
- 不会释放锁
- 可以在任意地方使用
Lock锁
由三个子类提供对象
重要方法
- lock.lock()
- trylock()
- unlock()
公平锁与非公平锁
Synchronized和Lock的区别
- Synchronized是java的内置关键字,Lock是java类
- Synchronized无法获取锁的状态,Lock可以
- Synchronized会自动释放锁,Lock不会
- Synchronized如果阻塞会一直等待,Lock则不会
- Synchronized锁是非公平锁,Lock可以直接设置
- Synchronized适合锁少量的代码同步问题,Lock适合大量
Condition
用于替代Synchronized以提供更高级的特征
由Lock的对象的lock.newCondition()提供对象
8锁问题
Synchronized修饰的方法锁的对象是方法的调用者
一个对象有一把锁
static的Synchronized表示锁的是类.Class
类.Class的锁和对象的锁不是同一把锁
Callable(泛型接口,泛型的类型为call方法的返回值)
可以有返回值
可以抛出异常
call()方法
有缓存,可能需要等待,get()可能会阻塞
关于Thread和Callable的关系
JUC常用三大辅助类
CountDownLatch(减法计数)
//减法计数器 CountDownLatch countDownLatch = new CountDownLatch(8); countDownLatch.countDown();//每执行一次就减1 countDownLatch.await();//当减到0的时候就唤醒,然后向下执行
CyclicBarrier(加法计数)
public CyclicBarrier(int parties) public CyclicBarrier(int parties, Runnable barrierAction)
await()方法告知CyclicBarrier线程已经到达,然后阻塞
Semaphore
ReadWriteLock
已知所以实现类:ReentrantReadWriteLock
两个方法
- writeLock()
- readLock()
阻塞队列
ArrayBlockingQueue
SynchronousQueue
不储存元素,put()一个元素之后就要take()出来,否则就会阻塞
线程池
Executos
Executors.newCachedThreadPool();//可升缩大小 Executors.newFixedThreadPool(5);//固定大小 Executors.newSingleThreadExecutor();//但个线程
三大方法
七大参数
(1)corePoolSize:线程池中的常驻核心线程数。
(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1。
- CPU密集型,获取电脑核数(Runtime.getRuntime().availableProcessors();)
- IO密集型,为耗时IO线程的两倍
(3)keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。
(4)unit:keepAliveTime的单位。
(5)workQueue:任务队列,被提交但尚未被执行的任务。
(6)threadFactory(Executors.defaultThreadFactory();):表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可。
(7)handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略。
流程分析
- 线程池中线程数小于corePoolSize时,新任务将创建一个新线程执行任务,不论此时线程池中存在空闲线程;
- 线程池中线程数达到corePoolSize时,新任务将被放入workQueue中,等待线程池中任务调度执行;
- 当workQueue已满,且maximumPoolSize>corePoolSize时,新任务会创建新线程执行任务;
- 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理;
- 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收该线程;
- 如果设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收;
一:corePoolSize 详细描述
(1)在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近视理解为今日当值线程。
(2)当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中。
二:最大线程数(maximumPoolSize):该参数定义了一个线程池中最多能容纳多少个线程。当一个任务提交到线程池中时,如果线程数量达到了核心线程数,并且任务队列已满,不能再向任务队列中添加任务时,这时会检查任务是否达到了最大线程数,如果未达到,则创建新线程,执行任务,否则,执行拒绝策略。可以通过源码来看一下。如下:可以看出,当调用submit(Runnable task)方法,将任务提交到线程池中时,会调用execute()方法去执行任务,在该方法内,会进行核心线程数,任务队列的判断,最后决定是执行或者是拒绝。总结起来就是:最大线程数参数,是在已经达到核心线程池参数,并且任务队列已经满的情况下,才去判断该参数。
三:keepAliveTime 详细描述
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze。
四:系统默认的拒绝策略有以下几种:
- AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
- DiscardPolicy:直接抛弃不处理。
- DiscardOldestPolicy:丢弃队列中最老的任务。
- CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理。
四大函数式接口
Function<T,R> 函数式接口: R apply(T t);
Predicate<T> 断定型接口:boolean test(T t); 有一个输入参数,返回值只能是 布尔值!
Consumer<T> 消费型接口:void accept(T t); 只有输入,没有返回值
Supplier<T> 供给型接口: T get(); 没有参数,只有返回值…
ForkJoin
工作窃取算法
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点:
充分利用线程进行并行计算,并减少了线程间的竞争。
工作窃取算法的缺点:
在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。
Fork/Join框架局限性:
对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程查找其他未被执行的任务,并开始执行这些未被执行的任务,通过这种方式,线程充分利用它们的运行时间来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性。
(1)任务只能使用Fork和Join操作来进行同步机制,如果使用了其他同步机制,则在同步操作时,工作线程就不能执行其他任务了。比如,在Fork/Join框架中,使任务进行了睡眠,那么,在睡眠期间内,正在执行这个任务的工作线程将不会执行其他任务了。
(2)在Fork/Join框架中,所拆分的任务不应该去执行IO操作,比如:读写数据文件。
(3)任务不能抛出检查异常,必须通过必要的代码来处理这些异常。
ForkJoinPool里面的方法
- public void execute(ForkJoinTask<?> task)
- public void execute(Runnable task)
- public <T> T invoke(ForkJoinTask<T> task)
- public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
- public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
- public <T> ForkJoinTask<T> submit(Callable<T> task)
- public <T> ForkJoinTask<T> submit(Runnable task, T result)
- public ForkJoinTask<?> submit(Runnable task)
异步回调
无返回值
有返回值
其父类的一部分方法
JMM
8锁
当主存的值被另一个线程修改时
具体实例
此时线程不会停下来,因为拿不到主存更新的num值
volatile
- 保证可见性
- 不保证原子性
- 禁止指令重排
unsafe
一个特殊的类,java无法操作内存,但可以调用c++,然后由c++操作内存
集合
如何重写hashCode
整理出你判断对象相等的属性,然后取一个尽可能小的正整数(尽可能小时怕最终得到的结果超出了整型int的取数范围),这里我取了17,(好像在JDK源码中哪里看过用的是17),然后计算17*属性的hashcode+其他属性的hashcode,重复步骤。
@Override public int hashCode() { int result = name.hashCode(); result = 17 * result + sex.hashCode(); result = 17 * result + age.hashCode(); return result;
for循环、增强for循环和迭代器的区别
- 迭代器是用于方便集合遍历的,实现了Iterable接口的集合都可以使用迭代器来遍历。使用迭代器遍历元素时,除了查看之外,只能做remove操作。
- 增强for循环,内部使用的是迭代器,所以它的操作对象是数组和可以使用迭代器的集合。遍历时只能查看,无法修改、删除、增加。所以如果需要对遍历的对象做增删修改的操作,使用普通的for循环来操作。
常用集合的分类:
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
list和set的区别
list集合的特点
- ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
- LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
- Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
set集合的特点
- HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
- LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
- TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;
map集合
HashMap 非线程安全
- HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。
- TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。
HashMap:适用于Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
线程安全集合类与非线程安全集合类
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
StringBuilder是非线程安全的,StringBuffer是线程安全的。
数据结构
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
文件操作
构造方法
public File(String pathname);//主要在Java EE的开发之中
public File(File parent, String child);//主要在Android开发之中
获取文件的基本信息
除了以上的常用的方法之外,在File类之中还可以通过以下的方法取得一些文件的基本信息:
(1)取得文件的名称:
public String getName();
(2)给定的路径是否是文件夹:
public boolean isDirectory();
(3)给定的路径是否是文件:
public boolean isFile();
(4)是否是隐藏文件:
public boolean isHidden();
(5)文件的最后一次修改日期:
public long lastModified();
(6)取得文件大小:
public long length();
字节流与字符流
(1)字节操作流:OutputStream、InputStream;
(2)字符操作流:Writer、Reader。
字节流
不管是写入还是读取,都是对byte()进行操作
OutputStream和InputStream是字节流的两个顶层父类。让他们提供了输出流类和输入流类通用API,字节流一般用于读写二进制数据
OutputStream output = new FileOutputStream(file, true);对原有数据不覆盖
OutputStream output = new FileOutputStream(file);会对原有数据覆盖
- 写入单个字节数据:public abstract void write(int b) throws IOException;
- 写入一组字节数据:public void write(byte[] b) throws IOException;
- 写入部分字节数据:public void write(byte[] b, int off, int len) throws IOException;
import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { //第一步:定义文件路径 File file = new File("D:"+File.separator + "demo"+ File.separator + "test.txt"); if(!file.getParentFile().exists()){ file.getParentFile().mkdirs(); } //第二步:实例化输出流 OutputStream output = new FileOutputStream(file); String data = "hello world !\r\nhello world !\r\nhello world !\r\nhello world !"; // 第三步:输出数据,要将数据变为字节数组输出 output.write(data.getBytes()); //第四步:关闭资源 output.close(); } }
InputStream input = new FileInputStream(file);
(1)读取单个字节:
public abstract int read() throws IOException;
注意:每次执行read()方法都会读取一个数据源的指定数据,如果现在发现已经读取到了结尾返回-1;
(2)将读取的数据保存到字节数组中:
public int read(byte[] b) throws IOException;
注意:如果现在要读取的数据小于byte的数据,这个时候read()方法的返回值int返回的是数据个数,如果数据已经读完了,则这个时候的int返回的是-1;
(3)将读取的数据保存在部分字节数组中:
public int read(byte[] b, int off, int len) throws IOException;
import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { //第一步:定义文件路径 File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定义文件路径 if (file.exists()) { // 文件存在则可以读取 //第二步:实例化输入流 InputStream input = new FileInputStream(file); //第三步:读取数据到字节数组 byte data[] = new byte[1024]; // 假设要读的长度是1024 int len = input.read(data); // 读取数据,返回读取个数 //第四步:关闭资源 input.close(); System.out.println("读取的数据是:【" + new String(data, 0, len) + "】"); } } }
字符流
字符输出流:Writer
写入字符串不需要将字符串转换为字节形式
import java.io.File; import java.io.FileWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定义文件路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs();// 创建父目录 } Writer out = new FileWriter(file); String data = "Hello World ."; out.write(data); // 直接写入字符串 out.close(); // 关闭 } }
字符输入流:Reader
读取方法:public int read (char[] cbuf) throws IOException;
import java.io.File; import java.io.FileReader; import java.io.Reader; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定义文件路径 if (file.exists()) { Reader in = new FileReader(file); // 字符输入流 char data[] = new char[1024]; // 开辟数组 int len = in.read(data); // 读取数据 in.close(); System.out.println("读取数据内容:【" + new String(data, 0, len) + "】"); } } }
完成一次文件复制
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { File inFile = new File("D:" + File.separator + "demo" + File.separator + "test.zip"); // 定义文件路径 File outFile = new File("D:" + File.separator + "demo" + File.separator + "test2.zip"); // 定义文件路径 long start = System.currentTimeMillis(); if (!inFile.exists()) { // 源文件不存在 System.out.println("源文件不存在!"); System.exit(1); // 程序退出 } if(!outFile.getParentFile().exists()){ outFile.getParentFile().mkdirs(); } InputStream input = new FileInputStream(inFile); OutputStream output = new FileOutputStream(outFile); int temp = 0;//保存每次读取的个数 byte data[] = new byte[4096]; // 每次读取4096字节 while ((temp = input.read(data)) != -1) { // 将每次读取进来的数据保存在字节数组里,并返回读取的个数 output.write(data, 0, temp); // 输出数组 } long end = System.currentTimeMillis(); System.out.println("拷贝完成,所花费的时间:" + (end - start) + "毫秒"); input.close(); output.close(); } }
对象序列化
- 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
- 一个静态变量不管是否被transient修饰,均不能被序列化。 第一点和第二点区别:transient修饰的变量是序列化到txt文件中之后无法被反序列化并打印出来,静态变量是根本无法序列化到txt文件之中。
- transient关键字只能修饰变量,而不能修饰方法和类。即使是变量,transient关键字也不能修饰局部变量,只能修改类变量,而且该类需要实现Serializable接口。
让对象可以被序列化的三种方式
默认序列化:定义类时实现Serializable接口即可,这个Serializable接口是一个空接口,没有需要实现的方法。作用是标记该类的对象可以被序列化,启用其序列化功能。通过调用ObjectOutputStream和ObjectInputStream的方法来对该对象进行序列化和反序列化。
类自定义序列化方式一:
定义类时,实现Serializable接口,并在类中定义:
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
这两个方法,在方法中通过对象输入流参数、对象输出流参数进行自定义的内容输出。这样通过对象输出流和对象输入流的输入输出方法序列化和反序列化对象时
会自动调用类中定义的writeObject和readObject方法而不是默认的序列化和反序列化方法。
类自定义序列化方式二:
实现Externalnalizable接口(继承自 Serializable接口),并且在类中实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,在方法中定义类对象自定义
的序列化和反序列化操作。这样通过对象输出流和对象输入流的输入输出方法序列化和反序列化对象时会自动调用类中定义的readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法。
序列化和反序列化的使用步骤
- 对象序列化包括如下步骤:
- 创建类,实现Serializable接口或者Externalizable接口,实现相应的序列化和反序列化方法(也可采取默认方法);
- 创建对象输出流ObjectOutputStream对象并在构造参数中指定流的输出目标(比如一个文件),通过objectOutputStream.writeObject(obj)把对象序列化并输出到流目标;
- 在需要提取对象处:创建对象输入流ObjectInputStream对象并在构造参数中指定流的来源,然后通过readObject()方法获取对象,并通过强制类型转换赋值给类对象引用。
Java序列化的特殊情况
- 静态变量和transient关键字修饰的变量不能被序列化;
- 反序列化时要按照序列化的顺序重构对象:如先序列化A后序列化B,则反序列化时也要先获取A后获取B,否则报错。
- 序列化ID的作用:虚拟机是否允许对象反序列化,不仅取决于该对象所属类路径和功能代码是否与虚拟机加载的类一致,而是主要取决于对象所属类与虚拟机加载的该类的序列化 ID 是否一致。
- 自定义序列化方法的应用场景:对某些敏感数据进行加密操作后再序列化;反序列化对加密数据进行解密操作。
- 重复序列化:同一个对象重复序列化时,不会把对象内容再次序列化,而是新增一个引用指向第一次序列化时的对象而已。
其他序列化手段
- 把对象包装成JSON格式进行序列化
- 用XML格式序列化
- 采用第三方插件(如:ProtoBuf)进行序列化
关于serialVersionUID,一般都会指定序列化版本号,以确保将来改动类时可以正常使用
static final long serialVersionUID = 1656156165L;
序列化和反序列化的注意点
①对象状态:序列化时,只对对象的状态进行保存,而不管对象的方法;
②父类子类:当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
③引用对象:当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
④private域和socket thread类:并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:
安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;
资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;
⑤static和transient:声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据(序列化的仅仅是对象的状态,没有对象的方法,也没有类的状态)。
⑥显式serialVersionUID用于完成序列化和反序列化,还有不同版本的兼容:序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:
在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
⑦Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;
⑧可以使用序列化深拷贝:如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存,这是能用序列化解决深拷贝的重要原因(注意:浅拷贝请使用Clone接口的原型模式)