《Java核心技术卷二》笔记(二)文件操作和内存映射文件
文件操作
上一篇已经总结了流操作,其中也包括文件的读写。文件系统除了读写以为还有很多其他的操作,如复制、移动、删除、目录浏览、属性读写等。在Java7之前,一直使用File类用于文件的操作。Java7提供了Path,Paths,Files等类,使文件操作变得简单和全面。此外还有很多第三方库也提供了文件操作的便捷类如common.io中的FileUtils类,Ant api提供的FileSet等类。
1.File类的使用
Java7之前版本中,File类即代表了路径对象也封装了文件的绝大部分操作。
File
- File(String pathname)
- File(String parent, String child)
- File(File parent, String child)
- File(URI uri)
- URI toURI()
- Path toPath()
- String getPath()/toString()
- String getName() 最后一段
- String getParent()
- File getParentFile()
- String getAbsolutePath()
- File getAbsoluteFile()
- String getCanonicalPath()
- File getCanonicalFile()
- boolean isAbsolute()
- boolean isFile()
- boolean isDirectoy()
- boolean exists()
- long length()
- boolean createNewFile()
- boolean delete()
- void deleteOnExit()
- boolean mkdir()
- boolean mkdirs()
- boolean renameTo(File dst)
- File createTempFile(String prefix, String suffix, File directory)
- File createTempFile(String prefix, String suffix)
- String[] list()
- String[] list(FilenameFilter filter)
- File[] listFiles()
- File[] listFiles(FilenameFilter filter)
- File[] listFiles(FileFilter filter)
- boolean isHidden()
- long lastModified()
- boolean canRead()
- boolean canWrite()
- boolean canExecute()
- boolean setLastModified(long time)
- boolean setReadOnly()
- boolean setWritable(boolean writable, boolean ownerOnly)
- boolean setWritable(boolean writable)
- boolean setReadable(boolean readable, boolean ownerOnly)
- boolean setReadable(boolean readable)
- boolean setExecutable(boolean executable, boolean ownerOnly)
- boolean setExecutable(boolean executable)
- long getTotalSpace() 返回文件所在分区的总大小
- long getFreeSpace()
- long getUsableSpace()
2.Path接口和Files类的使用
Java7中提供了Path接口代表了文件系统路径对象,而Files类封装了文件系统的绝大部分操作。
基本用法
Paths 使用默认的FileSystem类生成Path对象
- static Path get(String first, String... more) 拼出路径
- static Path get(URI uri) 将URI对象转化成Path对象
File类的Path toPath()方法也可以将File对象转化成Path对象。
Path 路径对象
- File toFile()
- URI toUri()
- String toString()
- Path normalize() 移除., ..等冗余路径元素
- Path toAbsolutePath() 返回等价的绝对路径
- Path relativize(Path other) 返回相对于other的相对路径
- Path getParent() 返回父路径
- Path getFileName() 返回路径最后一个部件
- Path getRoot() 返回根路径
- Path resolve(Path/String other) 如果other为绝对路径,返回other;否则,将other接在this路径的后面返回。
- Path resolveSibling(Path/String other) 如果other为绝对路径,返回other;否则,将other接在this的父路径后面返回。
- boolean startsWith(Path/String other)
- boolean endsWith(Path/String other)
files类方法中涉及的Option选项用的是不定参数,同时使用多个选项的方式为:
Files.copy(src, dst, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
Files
- static byte[] readAllBytes(Path path) 不太适合大文件
- static List<String> readAllLines(Path path, Charset cs) 不太适合大文件
- static Path write(Path path, byte[] bytes, OpenOption...options) 如StandardOpenOption.APPEND|READ|WRITE....
- static Path write(Path path, Iterable<? extends CharSequence>, Charset cs, OpenOption... options)
- static InputStream newInputStream(Path p, OpenOption...options) 将文件打开为输入流
- static OutputStream newOutputStream(Path p, OpenOption...options) 将文件打开为输出流
- static BufferedReader newBufferedReader(Path path, Charset cs)
- static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption...options)
- static Path copy(Path src, Path dst, CopyOption...options) 如StandardCopyOption.REPLACE_EXISTING|COPY_ATTRIBUTES|ATOMIC_MOVE
- static Path copy(Path src, OutputStream os )
- static Path copy(InputStream src, Path dst, CopyOption...options)
- static Path move(Path src, Path dst, CopyOption...options)
- static void delete(Path path) 删除的文件不存在会抛出异常,下面这个方法更省事
- static boolean deleteIfExists(Path path)
- static Path createDirectory(Path dir, FileAttribute<?> ...attrs) 不会自动创建中间目录
- static Path createDirectorys(Path dir, FileAttribute<?> ...attrs) 自动创建中间目录
- static Path createFile(Path path, FileAttribute<?> ...attrs) 不会自动创建中间目录,文件已经存在也会抛出异常,检查文件存在和创建文件是原子操作,其他程序在这个过程中也无法创建文件。
- static Path createTempFile(Path path, String prefix, String suffix, FileAttribute<?>...attr) 创建临时文件,可以固定文件名前缀或(和)后缀,中间部分随机生成。未指定Path的重载方法会自动在适当的临时文件目录中创建临时文件。
- static long size(Path path) 文件字节数
- static boolean exists(Path path, LinkOption...option)
- static boolean noExists(Path path, LinkOption...option)
- static boolean isHidden(Path path)
- static boolean isReadable(Path path)
- static boolean isWritable(Path path)
- static boolean isExecutable(Path path)
- static boolean isRegularFile(Path path, LinkOption...option)
- static boolean isDirectory(Path path, LinkOption...option)
- static boolean isSymbolicLink(Path path)
- static UserPrincipal getOwner(Path path, LinkOption...option)
- static Path setOwner(Path path, UserPrincipal owner)
- static Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption...option)
- static Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms)
- static FileTime getLastModifiedTime(Path path, LinkOption...option)
- static Path setLastModifiedTime(Path path, FileTime time)
- static <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
- static Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options)
- static Object getAttribute(Path path, String attribute, LinkOption...options)
- static Path setAttribute(Path path, String attribute, Object value, LinkOption...options)
- static DirectoryStream<Path> newDirectoryStream(Path path) 生成的DirectoryStream类实现了Iterable<T>接口,可以用来迭代目录内容
- static DirectorySteram<Path> newDirectoryStream(Path path, String glob) 生成的DirectoryStream可以迭代与glob模式匹配的目录内容(glob模式见后面)
- static DirectoryStream<Path> newDirectoryStream(Path path, Filter<? super Path> filter) 生成的DirectoryStream可以迭代与过滤器匹配的目录内容
- static Path walkFileTree(Path path, FileVisitor<? super Path> visitor) 遍历目录内容
- static Path walkFileTree(Path path, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)
文件属性
通过读取文件属性对象可以获取文件的各种属性信息(直接使用Files类的一些方法也可以获取或设置部分属性信息)。
BasicFileAttributes接口描述了文件的通用属性(FileTime类型的创建/最后一次访问/最后一次修改时间,常规文件/目录/符号链接,文件大小,文件主键),通用属性获取方式为: BasicFileAttributes attributes=Files.readAttributes(path, BasicFileAttributes.class);
Posix兼容文件系统还可以获取Posix属性:PosixFileAttributes attributes=Files.readAttributes(path, PosixFileAttributes.class);
PosixFilePermission为枚举类型,对应Posix的9种权限。
BasicFileAttributes
- FileTime creationTime()
- FileTime lastAccessTime()
- FileTime lastModifiedTime()
- boolean isRegularFile()
- boolean isDirectory()
- boolean isSymoblicLink()
- boolean isOther() 上面三种类型都不是
- long size()
- Object fileKey() 文件唯一标识
PosixFileAttributes implements BasicFileAttributes
- UserPrincipal owner()
- UserPrincipal group()
- Set<PosixFilePermission> permissions()
目录迭代
File类的list方法在目录包含大量的文件时效率较低,Files提供了改进的方法Directory newDirectoryStream(Path dir) 可以迭代当前目录中的对象(不包含子目录中对象)。该方法还有一个支持glob模式的重载和一个支持过滤器的重载。
Glob模式
- * 当前目录中匹配0个或多个字符
- ** 所有子目录中匹配0个或多个字符
- ? 匹配一个字符
- [...] 匹配一个字符集合,可用连字符 - 和取反符 ' ,如[0-9A-Za-z], ['0-9]
- {.. , ..} 匹配由 , 分隔的任意选项
- \ 转义glob特殊字符
DirectoryStream<Path> iter=Files.newDirectoryStream(Paths.get("H://zipTest"));
DirectoryStream<Path> iter1=Files.newDirectoryStream(Paths.get("H://zipTest"),"*.txt"); DirectoryStream<Path> iter2=Files.newDirectoryStream(Paths.get("H://zipTest"),new DirectoryStream.Filter<Path>(){
@Override
public boolean accept(Path entry) throws IOException {
return !Files.isDirectory(entry);
}}); System.out.println("---all---");
for(Path p: iter)
System.out.println(p.toString()); System.out.println("---*.txt---");
for(Path p: iter1)
System.out.println(p.toString()); System.out.println("---only file---");
for(Path p: iter2)
System.out.println(p.toString());
目录遍历
遍历目录中以及所有子目录中所有项目,使用walkFileTree方法,该方法需要传入一个FileVisitor接口的对象。SimpleFileVisitor是一个实现了该接口的便捷类,该类中visitFileFailed方法会抛出异常终止遍历,其他方法什么都不做。
FileVisitor<T>
- FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) 一个目录被处理前被调用
- FileVisitResult postVisitDirectory(T dir, IOException exc) 一个目录被处理后被调用
- FileVisitResult visitFile(T file, BasicFileAttributes attrs) 遇到一个文件时调用(T为Path)
- FileVisitResult visitFileFailed(T file, IOException exc) 访问文件发生错误时调用,如无权限
walkFileTree遍历时会读取项目的属性用来判断该项目是目录还是文件,读取到的属性值就顺便传给FileVisitor的方法,FileVisitor的方法里就不需要手动去读属性了。
FileVisitor的每个方法都返回FileVisitResult枚举,指示如何进行后续操作。
FileVisitorResult
- CONTINUE 继续访问下一个项目
- TERMINATE 终止遍历
- SKIP_SUBTREE 跳过这个目录下的所有子项,继续遍历
- SKIP_SIBLINGS 跳过这个目录的所有平级兄弟项目,继续遍历
Files.walkFileTree(Paths.get("H://zipTest"), new SimpleFileVisitor<Path>()
{
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
System.out.println(dir.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
System.out.println("\t"+file.toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
{
return FileVisitResult.CONTINUE;
}
});
zip文件系统
Path、Files等类默认都是使用的当前操作系统的磁盘文件系统类型,也可以使用其他类型的文件系统。
FileSystems.newFileSystem(xxx)方法可以创建一个文件内的文件系统,然后使用和普通文件系统一样的方式操作该文件的内容。 (基于一个文件创建文件系统时会遍历 FileSystemProvider的installedProviders方法返回的所有FileSystemProvider,查找一个能创建该文件系统的FileSystemProvider,没有找到的话还查找ClassLoader能加载的文件系统...)
直接用Paths.get(xxx)方法获取是在默认文件系统里的路径(等价于FileSystems.getDefault().getPath(xxx); ),而使用FileSystem对象的getPath()方法获取的路径就是该文件系统中的路径。
FileSystem fs=FileSystems.newFileSystem(Paths.get("ziptest.zip"), null);
Path copyFile=Paths.get("copyfile.txt");
Files.deleteIfExists(copyFile);
Files.copy(fs.getPath("textfile.txt"), copyFile); Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
System.out.println(file.toString());
return FileVisitResult.CONTINUE;
}
});
fs.close();
3.底层的文件系统
FileSystems
FileSystem
留坑待填...
内存映射文件
内存映射文件只指将整个文件或者文件的一部分映射到内存中,这个文件就可以像内存数组一样访问。内存映射速度比带缓存的流还要快一点(带缓冲的流可以极大提高读写速度)。内存映射适用于需要随机访问大文件,对应顺序对入的中小文件没必要使用。
内存映射文件会用到FileChannel类,对文件的读写都转化成了对内存缓冲区的操作。内存映射文件的操作:首先是建立文件通道,然后调用map方法获取映射的内存缓冲区,最后操作内存缓冲区即可(缓冲区中的更改会在适当的时候和通道关闭时写回存储设备)。
FileChannel类是对磁盘文件的一种抽象,读写操作都是与缓冲区交互,主要用在内存映射,文件加锁机制,以及文件间快速数据传递等操作系统特性。
FileChannel
- static FileChannel open(Path path, OpenOption... options) 也使用Files类介绍中提到的StandardOpenOption
- static FileChannel open(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
- long size() 文件大小
- long position() 返回文件指针位置
- void position(long pos) 设置文件指针位置
- void force(boolean metaData) 强制将文件的变更同步到存储设备
- FileChannel truncate(long size) 截断
- int read(ByteBuffer dst)... 从通道读入内容到缓冲区
- int write(ByteBuffer src)... 将缓冲区内容写入通道
- long transferTo(xxx)
- long transferFrom(xxx)
- MappedByteBuffer map(MapMode, long startposition, long size) MapMode.READ_ONLY|READ_WRITE|PRIVATE
- void close() 关闭通道,如果有可写的内存映射,其中的更改会写回存储设备
FileInputStream
- FileChannel getChannel()
FileOutputStream
- FileChannel getChannel()
RandomAccessFile
- FileChannel getChannel()
CRC32 crc=new CRC32();
CRC32 crc1=new CRC32(); FileChannel fc=FileChannel.open(Paths.get("copyfile.txt"),StandardOpenOption.READ,StandardOpenOption.WRITE);
MappedByteBuffer buffer=fc.map(MapMode.READ_WRITE, 0, fc.size()); //随机访问缓冲区
for(int p=0; p<buffer.limit(); p++)
{
int c=buffer.get(p);
crc.update(c);
} //顺序访问缓冲区
for(int p=0; p<buffer.limit(); p++)
{
int c=buffer.get();
crc1.update(c);
} System.out.println(Long.toHexString(crc.getValue()));
System.out.println(Long.toHexString(crc1.getValue()));
内存缓冲区
前面的例子已经用到了缓冲区类,现在详述总结一下这些类。缓冲区类其实就是相同类型数据构成的一个数组,再加上对缓冲区操作的一些方法。
缓冲区类的共同特点:
- 都有一个固定的容量,不可改变。
- 有一个读写位置,下一个值在此位置读写。
- 有一个界限,超过它之后读写就没有意义。
- 一个可选的标记位置,可以移动到该位置重复读入或写出。
- 各个位置的关系为:0<=标记<=读写位置<=界限<=容量
- 写和读操作在缓冲区上反复交替进行,缓冲区被循环使用。
缓冲区类一般都使用静态的allocate()方法创建或者使用warp()方法将一个数组包装成缓冲区对象。
使用过程:
- 重置:调用clear()方法,缓冲区的位置=0,界限=容量。
- 写入:然后向缓冲区写入数据,直到缓冲区位置=容量(占满)或者数据已经写完。
- 反转:调用flip()方法,缓冲区界限=位置,位置=0
- 读取:从缓冲区读取数据,直到位置=界限。然后再重置,一直循环...
读写的过程中可以调用rewind()将位置重置为0,重头读写。或者调用mark()/reset()使用标记,重新读写部分内容。
java中缓冲区都继承自Buffer抽象类,继承关系如下(不包括StringBuffer)。
Buffer
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- ByteBuffer
Buffer
- Buffer clear() 重置(读写位置=0,界限=容量),准备写入
- Buffer flip() 反转(界限=读写位置,新读写位置=0),准备读取
- Buffer rewind() 界限不变,读写位置=0,可以重头读写
- Buffer mark() 在当前读写位置设置标记
- Buffer reset() 重置读写位置到标记,可以重新标记后的内容
- int remaining() 剩余可读或写的数量,即 limit-position
- boolean hasRemaining() 是否还可读或可写。即 position<limit ?
- int position() 返回当前的读写位置
- Buffer position(int newpos) 改变读写位置
- int capacity() 返回缓冲区容量
- int limit() 返回限制位置
- Buffer limit(int limit) 设置限制位置
- boolean hasArray() 该Buffer是否是包装了一个数组
- Object array() 返回该Buffer包装的数组
- int arrayOffset() 如果包装了数组,返回数组被用作缓冲的起始位置(可以将数组的一部分包装成缓冲区)。
- boolean isDirect()
- boolean isReadOnly()
最最常用的缓冲区类ByteBuffer和CharBuffer已经在前一篇中写过 : http://www.cnblogs.com/pixy/p/4779820.html
MappedByteBuffer
- boolean isLoaded()
- MappedByteBuffer load()
- MappedByteBuffer force()
文件加锁机制
文件加锁机制可以防止多个同时执行的程序修改同一个文件导致文件被破坏。文件加锁也通过FileChannel类实现。
文件锁是由整个Java虚拟机持有的,由同一个虚拟器启动的不同Java进程不能同时独占锁定同一个文件。
文件加锁机制依赖于底层文件系统,有些系统无法锁定,有些系统锁定后不能映射,有些系统关闭一个通道会是否对应文件上所有的锁(避免在一个文件上使用多个通道),网络文件系统尽量避免使用锁。
FileChannel
- FileLock lock() 阻塞直到获得锁
- FileLock lock(long position, long size, boolean shared) 锁定文件的一部分,阻塞直到获得锁(锁定区域之后新增内容不会被锁,除非size设为Long.MAX_VALUE), shared设为true时获得共享税(允许多个进程读入,阻止任何进程独占)。
- FileLock tryLock() 立刻返回,无法获得锁时返回null
- FileLock tryLock(long position, long size, boolean shared)
FileLock
- void close()
- boolean isShared() 返回是否为共享锁
链接文件
Path link = FileSystems.getDefault().getPath("realFile");
Path target = FileSystems.getDefault().getPath("linkfile"); //create hard link
Files.createLink(link, target); //create symbolic link
Files.createSymbolicLink(link, target); //create symbolic with file attribute
PosixFileAttributes attrs = Files.readAttributes(target, PosixFileAttributes.class);
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(attrs.permissions());
Files.createSymbolicLink(link, target, attr); boolean isSymbolciLink1 = Files.isSymbolicLink(link);
boolean isSymbolciLink2 = (boolean) Files.getAttribute(link, "basic:isSymbolicLink"); //get linked file
Path linkedPath=Files.readSymbolicLink(link);
《Java核心技术卷二》笔记(二)文件操作和内存映射文件的更多相关文章
- 笔记:I/O流-内存映射文件
内存映射文件时利用虚拟内存实现来将一个文件或者文件的一部分映射到内存中,然后整个文件就可以当作数组一样的访问,这个比传统的文件操作要快得多,Java 使用内存映射文件首先需要从文件中获取一个chann ...
- JAVA I/O(三)内存映射文件
<Java编程思想>中对内存映射文件有详细的介绍,此处仅做简单记录和总结.内存映射文件允许创建和修改因为太大而不能放入内存的文件. 1. 内存映射文件简单实例 import java.io ...
- Java NIO之内存映射文件——MappedByteBuffer
大多数操作系统都可以利用虚拟内存实现将一个文件或者文件的一部分"映射"到内存中.然后,这个文件就可以当作是内存数组来访问,这比传统的文件要快得多. 内存映射文件的一个关键优势是操作 ...
- C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 VC中进程与进程之间共享内存 .net环境下跨进程、高频率读写数据 使用C#开发Android应用之WebApp 分布式事务之消息补偿解决方案
C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing). ...
- 【WIN32进阶之路】:内存映射文件
第一章:源起 遇到一个问题,如果一个客户数据文件有2g大,客户要通过界面查询文件中的数据并用列表控件显示数据,要怎么处理这个文件才能让应用程序不会长时间无响应,客户感觉不到程序的卡顿? 第二章:解决 ...
- 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
本文背景: 在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用:根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制. 本 ...
- NIO之通道(Channel)的原理与获取以及数据传输与内存映射文件
通道(Channel) 由java.nio.channels包定义的,Channel表示IO源与目标打开的连接,Channel类似于传统的“流”,只不过Channel本身不能直接访问数据,Channe ...
- MemoryMappedFile 内存映射文件 msdn
http://msdn.microsoft.com/zh-cn/library/dd997372%28v=vs.110%29.aspx 内存映射文件 .NET Framework 4.5 其他版本 1 ...
- C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转
原文:C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing ...
随机推荐
- hadoop的关键进程
hadoop集群中主要进程有master: NameNode, ResourceManager,slaves: DataNode, NodeManager, RunJar, MRAppMas ...
- 【Android 进阶】临时卸载root和恢复root功能
[前言]为什么有这个需求? Q:首先,谈谈为啥想要root呢? A:有root才能有控制权,也才能折腾很多东西,比如:删删流氓软件,用用代理.软件自动安装等: Q:然后,那么为何又需要删除root呢? ...
- UML中的stereotype
在使用rose的时候.rose的类里面有个stereotype的选项.选择了不同的选项类会呈现不同的图形效果.这里对stereotype做一点总结, Stereotyp英文的原意是印刷中的铅字.比如, ...
- wp7 BaseDictionary<TKey, TValue>
/// <summary>/// Represents a dictionary mapping keys to values./// </summary>/// /// &l ...
- hdu 1806 rmq
找到一个区间内出现最多的数的次数 10 3 //10个数字三次询问 -1 -1 1 1 1 1 3 10 10 10 2 3 1 10 5 10 0 143 #include<cstdio> ...
- UVALive 6885 Flowery Trails 最短路枚举
题目连接: http://acm.hust.edu.cn/vjudge/problem/visitOriginUrl.action?id=129723 题意: 给你一个n点m图的边 1到n有多条最短路 ...
- transient的理解
用法解释 1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问. 2)transient关键字只能修饰变量,而不能修饰方法和类.注意,本地变量是不能被 ...
- 使用OUYA第一次启动OUYA
使用OUYA第一次启动OUYA 1.4 使用OUYA 初次使用OUYA时,其启动以后的设置过程耗时较长,也比较繁琐,因此本节将会对其做个详细介绍,让读者的使用过程更加顺利些!好的开端总归是一个不错的 ...
- http://blog.csdn.net/jun55xiu/article/details/43051627
http://blog.csdn.net/jun55xiu/article/details/43051627
- 递推DP 赛码 1005 Game
题目传送门 /* 递推DP:官方题解 令Fi,j代表剩下i个人时,若BrotherK的位置是1,那么位置为j的人是否可能获胜 转移的时候可以枚举当前轮指定的数是什么,那么就可以计算出当前位置j的人在剩 ...