Java I/O 扩展
Java I/O 扩展
标签: Java基础
NIO
Java 的
NIO
(新IO)和传统的IO有着相同的目的:输入
输出
.但是NIO使用了不同的方式来处理IO,NIO利用内存映射文件
(此处文件的含义可以参考Unix的名言一切皆文件
)来处理IO, NIO将文件或文件的一段区域映射到内存中(类似于操作系统的虚拟内存),这样就可以像访问内存一样来访问文件了.
Channel
和 Buffer
是NIO中的两个核心概念:
Channel
是对传统的IO系统的模拟,在NIO系统中所有的数据都需要通过Channel
传输;Channel
与传统的InputStream
OutputStream
最大的区别在于它提供了一个map()
方法,可以直接将一块数据映射到内存中.如果说传统的IO系统是面向流的处理, 则NIO则是面向块
的处理;Buffer
可以被理解成一个容器, 他的本质是一个数组; Buffer作为Channel与程序的中间层, 存入到Channel
中的所有对象都必须首先放到Buffer
中(Buffer
->Channel
), 而从Channel
中读取的数据也必须先放到Buffer
中(Channel
->Buffer
).
Buffer
从原理来看,
java.nio.ByteBuffer
就像一个数组,他可以保存多个类型相同的数据.Buffer
只是一个抽象类,对应每种基本数据类型(boolean除外)都有相应的Buffer类:CharBuffer
ShortBuffer
ByteBuffer
等.
这些Buffer除了ByteBuffer
之外, 都采用相同或相似的方法来管理数据, 只是各自管理的数据类型不同而已.这些Buffer类都没有提供构造器, 可以通过如下方法来得到一个Buffer对象.
// Allocates a new buffer.
static XxxBuffer allocate(int capacity);
其中ByteBuffer
还有一个子类MappedByteBuffer
,它表示Channel
将磁盘文件全部映射到内存中后得到的结果, 通常MappedByteBuffer
由Channel
的map()
方法返回.
Buffer中的几个概念:
- capacity: 该Buffer的最大数据容量;
- limit: 第一个不应该被读出/写入的缓冲区索引;
- position: 指明下一个可以被读出/写入的缓冲区索引;
- mark: Buffer允许直接将position定位到该mark处.
0 <= mark <= position <= limit <= capacity
Buffer中常用的方法:
方法 | 解释 |
---|---|
int capacity()
|
Returns this buffer’s capacity. |
int remaining()
|
Returns the number of elements between the current position and the limit. |
int limit()
|
Returns this buffer’s limit. |
int position()
|
Returns this buffer’s position. |
Buffer position(int newPosition)
|
Sets this buffer’s position. |
Buffer reset()
|
Resets this buffer’s position to the previously-marked position. |
Buffer clear()
|
Clears this buffer.(并不是真的清空, 而是为下一次插入数据做好准备 |
Buffer flip()
|
Flips this buffer.(将数据封存 ,为读取数据做好准备)
|
除了这些在Buffer
基类中存在的方法之外, Buffer的所有子类还提供了两个重要的方法:
put()
: 向Buffer中放入数据get()
: 从Buffer中取数据
当使用put/get方法放入/取出数据时, Buffer既支持单个数据的访问, 也支持(以数组为参数)批量数据的访问.而且当使用put/get方法访问Buffer的数据时, 也可分为相对和绝对两种:
相对
: 从Buffer的当前position处开始读取/写入数据, position按处理元素个数后移.绝对
: 直接根据索引读取/写入数据, position不变.
/*** @author jifang* @since 16/1/9下午8:31.*/
public class BufferTest {@Testpublic void client() {ByteBuffer buffer = ByteBuffer.allocate(64);displayBufferInfo(buffer, "init");buffer.put((byte) 'a');buffer.put((byte) 'b');buffer.put((byte) 'c');displayBufferInfo(buffer, "after put");buffer.flip();displayBufferInfo(buffer, "after flip");System.out.println((char) buffer.get());displayBufferInfo(buffer, "after a get");buffer.clear();displayBufferInfo(buffer, "after clear");// 依然可以访问到数据System.out.println((char) buffer.get(2));}private void displayBufferInfo(Buffer buffer, String msg) {System.out.println("---------" + msg + "-----------");System.out.println("position: " + buffer.position());System.out.println("limit: " + buffer.limit());System.out.println("capacity: " + buffer.capacity());}
}
通过
allocate()
方法创建的Buffer对象是普通Buffer,ByteBuffer
还提供了一个allocateDirect()
方法来创建DirectByteBuffer
.DirectByteBuffer
的创建成本比普通Buffer要高, 但DirectByteBuffer
的读取效率也会更高.所以DirectByteBuffer
适用于生存期比较长的Buffer.
只有ByteBuffer
才提供了allocateDirect(int capacity)
方法, 所以只能在ByteBuffer
级别上创建DirectByteBuffer
, 如果希望使用其他类型, 则可以将Buffer转换成其他类型的Buffer.
Channel
像上面这样使用Buffer
感觉是完全没有诱惑力的(就一个数组嘛,还整得这么麻烦⊙﹏⊙b).其实Buffer
真正的强大之处在于与Channel
的结合,从Channel
中直接映射一块内存进来,而没有必要一一的get/put.
java.nio.channels.Channel
类似于传统的流对象, 但与传统的流对象有以下两个区别:
Channel
可以直接将指定文件的部分或者全部映射成Buffer
- 程序不能直接访问
Channel
中的数据, 必须要经过Buffer
作为中间层.
Java为Channel接口提供了FileChannel
DatagramChannel
Pipe.SinkChannel
Pipe.SourceChannel
SelectableChannel
SocketChannel
ServerSocketChannel
. 所有的Channel
都不应该通过构造器来直接创建, 而是通过传统的InputStream
OutputStream
的getChannel()
方法来返回对应的Channel
, 当然不同的节点流获得的Channel
不一样. 例如, FileInputStream
FileOutputStream
返回的是FileChannel
, PipedInputStream
PipedOutputStream
返回的是Pipe.SourceChannel
Pipe.SinkChannel
;
Channel
中最常用的三个方法是MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)
read()
write()
, 其中map()
用于将Channel对应的部分或全部数据映射成ByteBuffer
, 而read/write有一系列的重载形式, 用于从Buffer中读写数据.
/*** @author jifang* @since 16/1/9下午10:55.*/
public class ChannelTest {private CharsetDecoder decoder = Charset.forName("utf-8").newDecoder();@Testpublic void client() throws IOException {try (FileChannel inChannel = new FileInputStream("save.txt").getChannel();FileChannel outChannel = new FileOutputStream("attach.txt").getChannel()) {MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0,new File("save.txt").length());displayBufferInfo(buffer, "init buffer");// 将Buffer内容一次写入另一文件的ChanneloutChannel.write(buffer);buffer.flip();// 解码CharBuffer之后输出System.out.println(decoder.decode(buffer));}}// ...
}
Charset
Java从1.4开始提供了java.nio.charset.Charset
来处理字节序列和字符序列(字符串)之间的转换, 该类包含了用于创建解码器和编码器的方法, 需要注意的是, Charset
类是不可变类.
Charset
提供了availableCharsets()
静态方法来获取当前JDK所支持的所有字符集.
/*** @author jifang* @since 16/1/10下午4:32.*/
public class CharsetLearn {@Testpublic void testGetAllCharsets() {SortedMap<String, Charset> charsetMap = Charset.availableCharsets();for (Map.Entry<String, Charset> charset : charsetMap.entrySet()) {System.out.println(charset.getKey() + " aliases -> " + charset.getValue().aliases() + " chaset -> " + charset.getValue());}}
}
执行上面代码可以看到每个字符集都有一些字符串别名(比如UTF-8
还有unicode-1-1-utf-8
UTF8
的别名), 一旦知道了字符串的别名之后, 程序就可以调用Charset的forName()
方法来创建对应的Charset对象:
@Test
public void testGetCharset() {Charset utf8 = Charset.forName("UTF-8");Charset unicode11 = Charset.forName("unicode-1-1-utf-8");System.out.println(utf8.name());System.out.println(unicode11.name());System.out.println(unicode11 == utf8);
}
在Java 1.7 之后, JDK又提供了一个工具类StandardCharsets
, 里面提供了一些静态属性来表示标准的常用字符集:
@Test
public void testGetCharset() {// 使用UTF-8属性Charset utf8 = StandardCharsets.UTF_8;Charset unicode11 = Charset.forName("unicode-1-1-utf-8");System.out.println(utf8.name());System.out.println(unicode11.name());System.out.println(unicode11 == utf8);
}
获得了Charset
对象之后,就可以使用decode()
/encode()
方法来对ByteBuffer
CharBuffer
进行编码/解码了
方法 | 功能 |
---|---|
ByteBuffer encode(CharBuffer cb)
|
Convenience method that encodes Unicode characters into bytes in this charset. |
ByteBuffer encode(String str)
|
Convenience method that encodes a string into bytes in this charset. |
CharBuffer decode(ByteBuffer bb)
|
Convenience method that decodes bytes in this charset into Unicode characters. |
或者也可以通过Charset
对象的newDecoder()
newEncoder()
来获取CharsetDecoder
解码器和CharsetEncoder
编码器来完成更加灵活的编码/解码操作(他们肯定也提供了encode
和decode
方法).
@Test
public void testDecodeEncode() throws IOException {File inFile = new File("save.txt");FileChannel in = new FileInputStream(inFile).getChannel();MappedByteBuffer byteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, inFile.length());// Charset utf8 = Charset.forName("UTF-8");Charset utf8 = StandardCharsets.UTF_8;// 解码// CharBuffer charBuffer = utf8.decode(byteBuffer);CharBuffer charBuffer = utf8.newDecoder().decode(byteBuffer);System.out.println(charBuffer);// 编码// ByteBuffer encoded = utf8.encode(charBuffer);ByteBuffer encoded = utf8.newEncoder().encode(charBuffer);byte[] bytes = new byte[(int) inFile.length()];encoded.get(bytes);for (int i = 0; i < bytes.length; ++i) {System.out.print(bytes[i]);}System.out.println();}
String类里面也提供了一个
getBytes(String charset)
方法来使用指定的字符集将字符串转换成字节序列.
使用WatchService
监控文件变化
在以前的Java版本中,如果程序需要监控文件系统的变化,则可以考虑启动一条后台线程,这条后台线程每隔一段时间去遍历一次指定目录的文件,如果发现此次遍历的结果与上次不同,则认为文件发生了变化. 但在后来的NIO.2中,Path
类提供了register
方法来监听文件系统的变化.
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events);
WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers);
其实是
Path
实现了Watchable
接口,register
是Watchable
提供的方法.
WatchService
代表一个文件系统监听服务, 它负责监听Path
目录下的文件变化.而WatchService
是一个接口, 需要由FileSystem
的实例来创建, 我们往往这样获取一个WatchService
WatchService service = FileSystems.getDefault().newWatchService();
一旦register
方法完成注册之后, 接下来就可调用WatchService
的如下方法来获取被监听的目录的文件变化事件:
方法 | 释义 |
---|---|
WatchKey poll()
|
Retrieves and removes the next watch key, or null if none are present. |
WatchKey poll(long timeout, TimeUnit unit)
|
Retrieves and removes the next watch key, waiting if necessary up to the specified wait time if none are yet present. |
WatchKey take()
|
Retrieves and removes next watch key, waiting if none are yet present. |
- 获取到
WatchKey
之后, 就可调用其方法来查看到底发生了什么事件, 得到WatchEvent
方法 | 释义 |
---|---|
List<WatchEvent<?>> pollEvents()
|
Retrieves and removes all pending events for this watch key, returning a List of the events that were retrieved. |
boolean reset()
|
Resets this watch key. |
WatchEvent
方法 | 释义 |
---|---|
T context()
|
Returns the context for the event. |
int count()
|
Returns the event count. |
WatchEvent.Kind<T> kind()
|
Returns the event kind. |
/*** @author jifang* @since 16/1/10下午8:00.*/
public class ChangeWatcher {public static void main(String[] args) {watch("/Users/jifang/");}public static void watch(String directory) {try {WatchService service = FileSystems.getDefault().newWatchService();Paths.get(directory).register(service,StandardWatchEventKinds.ENTRY_CREATE,StandardWatchEventKinds.ENTRY_DELETE,StandardWatchEventKinds.ENTRY_MODIFY);while (true) {WatchKey key = service.take();for (WatchEvent event : key.pollEvents()) {System.out.println(event.context() + " 文件发生了 " + event.kind() + " 事件!");}if (!key.reset()) {break;}}} catch (IOException | InterruptedException e) {throw new RuntimeException(e);}}
}
通过使用WatchService
, 可以非常优雅的监控指定目录下的文件变化, 至于文件发生变化后的处理, 就取决于业务需求了, 比如我们可以做一个日志分析器, 定时去扫描日志目录, 查看日志大小是否改变, 当发生改变时候, 就扫描发生改变的部分, 如果发现日志中有异常产生(比如有Exception/Timeout类似的关键字存在), 就把这段异常信息截取下来, 发邮件/短信给管理员.
Guava IO
- 平时开发中常用的IO框架有Apache的
commons-io
和GoogleGuava
的IO模块; 不过Apache的commons-io
包比较老,更新比较缓慢(最新的包还是2012年的); 而Guava则更新相对频繁, 最近刚刚发布了19.0版本, 因此在这儿仅介绍Guava对Java IO的扩展. - 使用Guava需要在
pom.xml
中添加如下依赖:
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>19.0</version>
</dependency>
最近我在写一个网页图片抓取工具时, 最开始使用的是Java的URL.openConnection()
+ IOStream
操作来实现, 代码非常繁琐且性能不高(详细代码可类似参考java 使用URL来读取网页内容). 而使用了Guava之后几行代码就搞定了网页的下载功能:
public static String getHtml(String url) {if (StringUtils.isBlank(url)) {return null;}try {return Resources.toString(new URL(url), StandardCharsets.UTF_8);} catch (IOException e) {LOGGER.error("getHtml error url = {}", url, e);throw new RuntimeException(e);}
}
代码清晰多了.
- 还可以使用
Resources
类的readLines(URL url, Charset charset, LineProcessor<T> callback)
方法来实现只抓取特定的网页内容的功能:
public static List<String> processUrl(String url, final String regexp) {try {return Resources.readLines(new URL(url), StandardCharsets.UTF_8, new LineProcessor<List<String>>() {private Pattern pattern = Pattern.compile(regexp);private List<String> strings = new ArrayList<>();@Overridepublic boolean processLine(String line) throws IOException {Matcher matcher = pattern.matcher(line);while (matcher.find()) {strings.add(matcher.group());}return true;}@Overridepublic List<String> getResult() {return strings;}});} catch (IOException e) {LOGGER.error("processUrl error, url = {}, regexp = {}", url, regexp, e);throw new RuntimeException(e);}
}
而性能的话, 我记得有这么一句话来评论STL的
STL性能可能不是最高的, 但绝对不是最差的!
我认为这句话同样适用于Guava; 在Guava IO中, 有三类操作是比较常用的:
- 对Java传统的IO操作的简化;
- Guava对
源
与汇
的支持; - Guava
Files
Resources
对文件/资源的支持;
Java IO 简化
- 在Guava中,用
InputStream/OutputStream
Readable/Appendable
来对应Java中的字节流和字符流(Writer
实现了Appendable
接口,Reader
实现了Readable
接口).并用com.google.common.io.ByteStreams
和com.google.common.io.CharStreams
来提供对传统IO的支持.
这两个类中, 实现了很多static方法来简化Java IO操作,如:
static long copy(Readable/InputStream from, Appendable/OutputStream to)
static byte[] toByteArray(InputStream in)
static int read(InputStream in, byte[] b, int off, int len)
static ByteArrayDataInput newDataInput(byte[] bytes, int start)
static String toString(Readable r)
/*** 一行代码读取文件内容** @throws IOException*/
@Test
public void getFileContent() throws IOException {FileReader reader = new FileReader("save.txt");System.out.println(CharStreams.toString(reader));
}
关于ByteStreams
和CharStreams
的详细介绍请参考Guava文档
Guava源与汇
- Guava提出源与汇的概念以避免总是直接跟流打交道.
- 源与汇是指某个你知道如何从中打开流的资源,如File或URL.
- 源是可读的,汇是可写的.
Guava的源有 ByteSource
和 CharSource
; 汇有ByteSink
CharSink
- 源与汇的好处是它们提供了一组通用的操作(如:一旦你把数据源包装成了ByteSource,无论它原先的类型是什么,你都得到了一组按字节操作的方法). 其实就源与汇就类似于Java IO中的
InputStream/OutputStream
,Reader/Writer
. 只要能够获取到他们或者他们的子类, 就可以使用他们提供的操作, 不管底层实现如何.
/*** @author jifang* @since 16/1/11下午4:39.*/
public class SourceSinkTest {@Testpublic void fileSinkSource() throws IOException {File file = new File("save.txt");CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);sink.write("- 你好吗?\n- 我很好.");CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);System.out.println(source.read());}@Testpublic void netSource() throws IOException {CharSource source = Resources.asCharSource(new URL("http://www.sun.com"), StandardCharsets.UTF_8);System.out.println(source.readFirstLine());}
}
获取源与汇
- 获取字节源与汇的常用方法有:
字节源 | 字节汇 |
---|---|
Files.asByteSource(File)
|
Files.asByteSink(File file, FileWriteMode... modes)
|
Resources.asByteSource(URL url)
|
- |
ByteSource.wrap(byte[] b)
|
- |
ByteSource.concat(ByteSource... sources)
|
- |
- 获取字符源与汇的常用方法有:
字符源 | 字符汇 |
---|---|
Files.asCharSource(File file, Charset charset)
|
Files.asCharSink(File file, Charset charset, FileWriteMode... modes)
|
Resources.asCharSource(URL url, Charset charset)
|
- |
CharSource.wrap(CharSequence charSequence)
|
- |
CharSource.concat(CharSource... sources)
|
- |
ByteSource.asCharSource(Charset charset)
|
ByteSink.asCharSink(Charset charset)
|
使用源与汇
- 这四个源与汇提供通用的方法进行读/写, 用法与Java IO类似,但比Java IO流会更加简单方便(如
CharSource
可以一次性将源中的数据全部读出String read()
, 也可以将源中的数据一次拷贝到Writer或汇中long copyTo(CharSink/Appendable to)
)
@Test
public void saveHtmlFileChar() throws IOException {CharSource source = Resources.asCharSource(new URL("http://www.google.com"), StandardCharsets.UTF_8);source.copyTo(Files.asCharSink(new File("save1.html"), StandardCharsets.UTF_8));
}@Test
public void saveHtmlFileByte() throws IOException {ByteSource source = Resources.asByteSource(new URL("http://www.google.com"));//source.copyTo(new FileOutputStream("save2.html"));source.copyTo(Files.asByteSink(new File("save2.html")));
}
其他详细用法请参考Guava文档
Files与Resources
上面看到了使用
Files
与Resources
将URL
和File
转换成ByteSource
与CharSource
的用法,其实这两个类还提供了很多方法来简化IO, 详细请参考Guava文档Resources
常用方法
Resources 方法 | 释义 |
---|---|
static void copy(URL from, OutputStream to)
|
Copies all bytes from a URL to an output stream. |
static URL getResource(String resourceName)
|
Returns a URL pointing to resourceName if the resource is found using the context class loader. |
static List<String> readLines(URL url, Charset charset)
|
Reads all of the lines from a URL. |
static <T> T readLines(URL url, Charset charset, LineProcessor<T> callback)
|
Streams lines from a URL, stopping when our callback returns false, or we have read all of the lines. |
static byte[] toByteArray(URL url)
|
Reads all bytes from a URL into a byte array. |
static String toString(URL url, Charset charset)
|
Reads all characters from a URL into a String, using the given character set. |
Files
常用方法
Files 方法 | 释义 |
---|---|
static void append(CharSequence from, File to, Charset charset)
|
Appends a character sequence (such as a string) to a file using the given character set. |
static void copy(File from, Charset charset, Appendable to)
|
Copies all characters from a file to an appendable object, using the given character set. |
static void copy(File from, File to)
|
Copies all the bytes from one file to another. |
static void copy(File from, OutputStream to)
|
Copies all bytes from a file to an output stream. |
static File createTempDir()
|
Atomically creates a new directory somewhere beneath the system’s temporary directory (as defined by the java.io.tmpdir system property), and returns its name. |
static MappedByteBuffer map(File file, FileChannel.MapMode mode, long size)
|
Maps a file in to memory as per FileChannel.map(java.nio.channels.FileChannel.MapMode, long, long) using the requested FileChannel.MapMode.
|
static void move(File from, File to)
|
Moves a file from one path to another. |
static <T> T readBytes(File file, ByteProcessor<T> processor)
|
Process the bytes of a file. |
static String readFirstLine(File file, Charset charset)
|
Reads the first line from a file. |
static List<String> readLines(File file, Charset charset)
|
Reads all of the lines from a file. |
static <T> T readLines(File file, Charset charset, LineProcessor<T> callback)
|
Streams lines from a File, stopping when our callback returns false, or we have read all of the lines. |
static byte[] toByteArray(File file)
|
Reads all bytes from a file into a byte array. |
static String toString(File file, Charset charset)
|
Reads all characters from a file into a String, using the given character set. |
static void touch(File file)
|
Creates an empty file or updates the last updated timestamp on the same as the unix command of the same name. |
static void write(byte[] from, File to)
|
Overwrites a file with the contents of a byte array. |
static void write(CharSequence from, File to, Charset charset)
|
Writes a character sequence (such as a string) to a file using the given character set. |
- 参考:
- Google Guava官方教程(中文版)
- Google Guava官方文档
Java I/O 扩展相关推荐
- Java SE 12扩展Switch语句/表达式完整指南
本文提供了Java SE 12扩展Switch语句/表达式的完整指南.文章详细介绍了扩展Java switch语句将其用作增强版switch语句或表达式.为帮助理解本文提供了具体案例. 本文要点 现在 ...
- Java URL协议扩展实现
2019独角兽企业重金招聘Python工程师标准>>> Java URL协议扩展实现 博客分类: java 在信息交互系统设计中,不乏有自定义通讯协议设计.本章会介绍如何利用 jav ...
- Java 8虚拟扩展方法
我一直关注Java 8 Lambda表达式项目的发展已经有一段时间了,我对其当前的进展状态感到非常兴奋. 我发现的最新"易于理解"的演示文稿是这样的: http://blogs.o ...
- java的整数扩展,浮点数扩展,字符扩展,转义字符,布尔值扩展
java的整数扩展,浮点数扩展,字符扩展,转义字符,布尔值扩展 public static void main(String[] args) {//整数扩展 进制 二进制0b 十进制 八进制0 十六进 ...
- java 获取文件扩展名_如何在Java中获取文件扩展名
java 获取文件扩展名 Sometimes while working with files, we need to process them differently based on their ...
- Java基础之扩展特性
Java基础之扩展特性 一.泛型 二.包装类 三.递归算法 四.异常处理 五.自定义异常 六.常用类 七.String 的正则表达式 八.Java 和 C++的区别 九.TCP/IP 初识 十.Soc ...
- java jsse ssl 调试_使用Java安全Socket扩展包(JSSE)实现SSL
使用Java安全Socket扩展包(JSSE)实现SSL 林琪;卢昱 [期刊名称]<装备学院学报> [年(卷),期]2002(013)003 [摘要]SSL是安全应用中一个非常重要的协议, ...
- java数组可扩展_[转载]Java数组扩容算法及Java对它的应用
Java数组扩容的原理 1)Java数组对象的大小是固定不变的,数组对象是不可扩容的. 2)利用数组复制方法可以变通的实现数组扩容. 3)System.arraycopy()可以复制数组. 4)Arr ...
- JAVA多线程之扩展ThreadPoolExecutor
欢迎支持笔者新作:<深入理解Kafka:核心设计与实践原理>和<RabbitMQ实战指南>,同时欢迎关注笔者的微信公众号:朱小厮的博客. 欢迎跳转到本文的原文链接:https: ...
最新文章
- C++ 对象动态建⽴和释放 new 和 delete
- Unity3d 去掉exe版本的边框
- CentOS文件权限管理
- java发送html模板
- 10条途径迅速提高你的生活
- wowza 技术交流群/ wowza 流媒体软件交流群
- SI4463模块使用心得(无线协议)
- IC卡(智能卡)APDU通讯总结
- Intellij IDEA 提示cannot find declaration to go to 解决方法
- “网络吸血鬼” Leech
- 供货肯德基与必胜客,千味央厨上市全靠“抱大腿”?
- python|简单实现英文单词大小写转化
- Delphi历史版本介绍(一)从Delphi1到Delphi7
- 200用户的oa文件服务器配置,oa服务器主要配置
- GPS北斗卫星授时设备(京准)
- SpringBoot 高级部分笔记4
- n枚硬币找出假币问题(包含一枚假币)
- 达人评测i5 1135g7和i7 8565u选哪个好
- 简化开发|Lombok神器带你消除冗余代码
- i2c-tools 测试i2c
热门文章
- Origin使用手册/笔记第二部分:数据的录入
- 利用PowerShell Empire实现Word文档DDE攻击控制(简单没啥用)
- Python内置函数——__import__ 的使用方法
- searchsploit漏洞查找工具使用指南(exploit-db.com 离线工具)
- Redis Info 命令
- C++网易云课堂开发工程师-拷贝构造,拷贝复制,析构函数
- SQL Server使用 LEFT JOIN ON LIKE进行数据关联查询
- 大数据应用之金融行业-互联网金融对传统银行业的冲击,狼真的来了
- System.DBNull.Value与Null的区别
- 重命名数据库解决“无法用排他锁锁定该数据库”