Я пытаюсь прочитать zip-файл, проверить, что он имеет некоторые необходимые файлы, и затем выпишите все правильные файлы к другому zip-файлу. Основное введение в java.util.zip имеет много измов Java, и я хотел бы сделать свой код более Scala-собственным. А именно, я хотел бы избежать использования vars
. Вот то, что я имею:
val fos = new FileOutputStream("new.zip");
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos));
while (zipIn.available == 1) {
val entry = zipIn.getNextEntry
if (entryIsValid(entry)) {
zipOut.putNewEntry(new ZipEntry("subdir/" + entry.getName())
// read data into the data Array
var data = Array[Byte](1024)
var count = zipIn.read(data, 0, 1024)
while (count != -1) {
zipOut.write(data, 0, count)
count = zipIn.read(data, 0, 1024)
}
}
zipIn.close
}
zipOut.close
Я должен добавить, что использую Scala 2.7.7.
d Я не думаю, что есть что-то особенно плохое в использовании классов Java, предназначенных для работы в императивном стиле, в том стиле, в котором они были разработаны. Идиоматическая Scala включает в себя возможность использовать идиоматическую Java так, как она была задумана, даже если стили немного противоречат друг другу.
Однако, если вы хотите - возможно, в качестве упражнения, а возможно, потому что это немного проясняет логику - сделать это более функциональным способом без var, вы можете это сделать. В 2.8 это особенно хорошо, поэтому, даже если вы используете 2.7.7, я дам ответ по 2.8.
Во-первых, нам нужно поставить задачу, что вы сделали не полностью, но давайте предположим, что у нас есть что-то вроде этого:
import java.io._
import java.util.zip._
import scala.collection.immutable.Stream
val fos = new FileOutputStream("new.zip")
val zipOut = new ZipOutputStream(new BufferedOutputStream(fos))
val zipIn = new ZipInputStream(new FileInputStream("old.zip"))
def entryIsValid(ze: ZipEntry) = !ze.isDirectory
Теперь, учитывая это, мы хотим скопировать zip-файл. Трюк, который мы можем использовать - это метод continually
в collection.immutable.Stream
. Он выполняет цикл с ленивой оценкой. Затем вы можете взять и отфильтровать результаты, чтобы завершить и обработать то, что вам нужно. Это удобный шаблон для использования, когда у вас есть что-то, что вы хотите сделать итератором, но это не так. (Если элемент обновляется сам, вы можете использовать .iterate
в Iterable
или Iterator
- это обычно даже лучше). Вот приложение к этому случаю, используемое дважды: один раз для получения записей, а другой - для чтения/записи фрагментов данных:
val buffer = new Array[Byte](1024)
Stream.continually(zipIn.getNextEntry).
takeWhile(_ != null).filter(entryIsValid).
foreach(entry => {
zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
Stream.continually(zipIn.read(buffer)).takeWhile(_ != -1).
foreach(count => zipOut.write(buffer,0,count))
})
}
zipIn.close
zipOut.close
Обратите внимание на .
в конце некоторых строк! Обычно я пишу это в одной длинной строке, но приятнее, когда это обернуто, чтобы вы могли видеть все это здесь.
На всякий случай, если это непонятно, давайте распакуем одно из применений continually
.
Stream.continually(zipIn.read(buffer))
Это просит продолжать вызывать zipIn.read(buffer)
столько раз, сколько необходимо, сохраняя целое число, которое получается в результате.
.takeWhile(_ != -1)
Это указывает, сколько раз необходимо, возвращая поток неопределенной длины, который завершится, когда достигнет -1
.
.foreach(count => zipOut.write(buffer,0,count))
Это обрабатывает поток, принимая каждый элемент по очереди (счетчик) и используя его для записи в буфер. Это работает немного коварно, поскольку вы полагаетесь на тот факт, что zipIn
только что был вызван для получения следующего элемента потока - если бы вы попытались сделать это снова, а не за один проход по потоку, это бы провалилось, поскольку buffer
был бы перезаписан. Но здесь все в порядке.
Итак, вот он: немного более компактный, возможно, более простой для понимания, возможно, менее простой для понимания метод, который является более функциональным (хотя побочных эффектов по-прежнему много). В 2.7.7, напротив, я бы сделал это на Java, потому что Stream.continually
недоступен, а накладные расходы на создание пользовательского Iterator
не стоят того для данного случая. (Однако это стоило бы сделать, если бы я собирался обрабатывать больше zip-файлов и мог повторно использовать код.)
Edit: Метод поиска доступных к переходу к нулю является несколько нечетким для обнаружения конца zip-файла. Я думаю, что "правильный" способ - это ждать, пока вы не получите null
обратно от getNextEntry
. Учитывая это, я отредактировал предыдущий код (там был takeWhile(_ => zipIn.available==1)
, который теперь является takeWhile(_ != null)
) и предоставил версию 2.7. 7 версию на основе итераторов (обратите внимание, насколько мал главный цикл, как только вы пройдете через работу по определению итераторов, которые, по общему признанию, используют переменные):
val buffer = new Array[Byte](1024)
class ZipIter(zis: ZipInputStream) extends Iterator[ZipEntry] {
private var entry:ZipEntry = zis.getNextEntry
private var cached = true
private def cache { if (entry != null && !cached) {
cached = true; entry = zis.getNextEntry
}}
def hasNext = { cache; entry != null }
def next = {
if (!cached) cache
cached = false
entry
}
}
class DataIter(is: InputStream, ab: Array[Byte]) extends Iterator[(Int,Array[Byte])] {
private var count = 0
private var waiting = false
def hasNext = {
if (!waiting && count != -1) { count = is.read(ab); waiting=true }
count != -1
}
def next = { waiting=false; (count,ab) }
}
(new ZipIter(zipIn)).filter(entryIsValid).foreach(entry => {
zipOut.putNextEntry(new ZipEntry("subdir/"+entry.getName))
(new DataIter(zipIn,buffer)).foreach(cb => zipOut.write(cb._2,0,cb._1))
})
zipIn.close
zipOut.close
Использование scala2.8 и хвостового рекурсивного вызова:
def copyZip(in: ZipInputStream, out: ZipOutputStream, bufferSize: Int = 1024) {
val data = new Array[Byte](bufferSize)
def copyEntry() {
in getNextEntry match {
case null =>
case entry => {
if (entryIsValid(entry)) {
out.putNextEntry(new ZipEntry("subdir/" + entry.getName()))
def copyData() {
in read data match {
case -1 =>
case count => {
out.write(data, 0, count)
copyData()
}
}
}
copyData()
}
copyEntry()
}
}
}
copyEntry()
}
Я бы попробовал что-то вроде этого (да, практически та же идея, что и у sblundy):
Iterator.continually {
val data = new Array[Byte](100)
zipIn.read(data) match {
case -1 => Array.empty[Byte]
case 0 => new Array[Byte](101) // just to filter it out
case n => java.util.Arrays.copyOf(data, n)
}
} filter (_.size != 101) takeWhile (_.nonEmpty)
Это можно упростить, как показано ниже, но мне это не очень нравится. Я бы предпочел, чтобы read
не мог возвращать 0...
Iterator.continually {
val data = new Array[Byte](100)
zipIn.read(data) match {
case -1 => new Array[Byte](101)
case n => java.util.Arrays.copyOf(data, n)
}
} takeWhile (_.size != 101)
Без хвостовой рекурсии я бы избегал рекурсии. Вы рискуете получить переполнение стека. Вы можете обернуть zipIn.read(data)
в scala.BufferedIterator[Byte]
и идти оттуда.