Java, Scala, .NET, Lisp, Python, IDE's, Hibernate, MATLAB, Mathematica, Physics & Other

суббота, 23 января 2010 г.

Осторожно: зомби атакуют!


Занимаясь своими повседневными делами люди забывают а реальной опасности, которая угрожает всем нам. Я говорю, конечно же, об атаке живых мертвецов, известных как зомби. Что делать если вдруг вирус, делающий из человека зомби, начнет распространятся среди людей? Как действовать отдельному человеку в такой ситуации? Какую стратегию, тактику применить людям чтобы выжить? Очевидно, все эти животрепещущие вопросы требует тщательного и серьезного исследования.. В заметке "Как убить зомби: Математика сопротивления" говорится о неком исследовании проведенном Робертом Смитом?. Выводы которые были получены в данной работе таковы: надо бить мертвяков решительно, и беспощадно. Совет конечно хорош, но звучит это так расплывчато и обобщенно.. А если зомби много, то может быть все-таки лучше бежать? А может надо использовать какую-то хитрость (ведь все же знают, что зомби тупые)? И если бить то чем? Например герой культового фильма "Живая мертвечина" очень оригинально догадался использовать против полчищ зомби газонокосилку.. Мне кажется, что проблема чисто математической модели атаки зомби в том, что в ней чрезвычайно трудно учесть разные тонкости и нюансы. Поэтому я решил построить модель программную. Реализована она была на языке java. Я попытался сделать эту модель как можно реалистичнее и ближе к тому, что может происходить на самом деле.

Ниже привожу краткую характеристику модели:
  • Модель пошаговая. Каждый шаг соответствует периоду в 20 минут.
  • Есть 3 состояния: человек, инфицированный и зомби.
  • У человека и зомби есть определенное количество жизней.
  • Будучи инфицированным, человек каждый свой ход теряет некоторое количество здоровья (болеет), потом когда умирает, то превращается в зомби.
  • Чтобы убить зомби надо отсечь ему голову, но люди об этом вначале не знают. Т.е. зомби якобы подыхает, но если у него целая голова, то он быстро регенерирует и встает.
  • Вообще у людей есть 4 уровня осведомленности: ничего не знает, знает что зомби напали, знает уязвимое место зомби, знает как окончательно убить зомби.
  • Впервые завидев зомби люди в панике бегут.
  • Всех кого встретит такой напуганный человек тоже узнают что зомби напали.
  • Люди могут объединятся в группы.
  • Кроме того люди будут стараться искать оружие; с некоторой вероятностью находят: нож или ружье (других видов пока нету).
  • Зомби, если встречают друг друга тоже объединяются в стаи (вместе жрать веселее).
  • Если встретятся две группы людей и зомби, то люди прикидывают свои шансы и взависимости от коэффициента смелости атакуют или спасаются бегством.
  • Во время битвы первыми идут в бой самые здоровые, сильные и более опытные люди.
  • Если человека ранят, то с некоторой вероятностью он становится инфицированным.
  • Если человека убьют, то зомби могут питаться его трупом, восстанавливая свое здоровье.
  • Если человек в группе собственноручно прибил (убил не до конца) определенное количество зомби, то ему открывается информация о том, что уязвимой точкой является голова.
  • Этим знанием человек делится с другими людьми в группе. Зная уязвимую точку люди начинают сражаться более эффективно.
  • Если человек прибивает еще большее количество зомбаков, то ему открывается знание о том как убить зомби.
  • Одной из стратегией людей является изоляция зараженных. Изоляция это например запереть в каком-нибудь подвале. Положительная сторона - инфицированные, когда переродятся в зомби, окажутся закрытыми от внешнего мира и не смогут навредить никому. Отрицательная сторона - инфицированные все еще может драться за людей какое-то время, а в изоляции он бездействует.
  • Также модель учитывает разделение людей по полу и возрасту. Пол и возраст влияют на 4 характеристики: жизненные силы, выносливость, скорость и смелость.
На входе модели: начальное количество людей, зараженных и зомби, на выходе - скрипт для MATLAB который рисует графики показывающие динамику развития событий. Вот пример для села из 100 человек, 10 из которых инфицированы.



Синим - люди, зеленым - зараженные, красным - зомби. Из рисунка видно что модель отработала почти 120 шагов (40 часов). Поначалу люди сопротивлялись плохо, но потом к 65 шагу перехватили инициативу. Возможно к тому времени кто-то догадался где находится уязвимая точка зомби - это стало переломным моментом.






Однако следует учесть, что модель является вероятностной, она содержит массу коэффициентов которые определяют лишь шанс наступления того или иного события. Так, если прогнать ее на тех же начальных условиях, то исход может оказаться совсем другим:



На этот раз люди были истреблены и все до единого превратились в ходячих мертвецов..

За частными случаями трудно проследить общую тенденцию. Могут ли все таки 100 человек выжить если 10 из них окажутся зараженными? Только что увидели два различных исхода. Так вот, для того чтобы увидеть общую картину, необходимо просто запустить модель N-е количество раз и взять среднее по всем графикам. Посмотрите на следующий рисунок.



людей: 100
инфицированных: 10
запусков: 1000

Анализируя эти графики можно сделать вывод что: зомби набирают силу примерно до 40-го шага (13 часов после инфицирования), люди в это время по большей части убегают и собираются в группы. После 40 шага наступает некий перелом, и люди постепенно истребляют зомби. Тот факт, что количество зомби не убывает в конце, означает что в некоторых из запусков модели зомби одержали верх.

Дальше предлагаю просто посмотреть и сравнить графики для разных начальных условий.


людей: 100
инфицированных: 5
запусков: 1000










людей: 100
инфицированных: 15
запусков: 1000










людей: 100
инфицированных: 10
запусков: 1000

* Все люди - мужчины.



людей: 100
инфицированных: 10
запусков: 1000

* Все люди - женщины.









людей: 100
инфицированных: 10
запусков: 1000

* Все люди - дети до 14 лет.









людей: 100
инфицированных: 10
запусков: 1000

* Все люди - в возрасте от 15 до 35 лет.









людей: 100
инфицированных: 10
запусков: 1000

* Все люди - в возрасте от 36 до 65 лет.








людей: 100
инфицированных: 10
запусков: 1000

* Все люди - в возрасте от 66 лет и старше.





Отлично, молодые мужчины сопротивляются зомби лучше чем женщины, дети и старики. Не великое это открытие. Какую же пользу можно извлечь из всего этого? Как я уже говорил модель имеет множество параметров. В том числе есть параметры влияющие на тактику людей. Это:
- Общий коэффициент смелости (маленький - люди будут всегда убегать, большой - безрассудно кидаться в атаку)
- Шанс объединится в группу и коэффициент увеличения этого шанса с каждым шагом. Смысл этих двух коэффициентов в следующем: если люди будут объединятся в группы сразу и ходить группами то больше шанс отбиться от зомби, однако, если они будут по одиночке то они смогут быстрее разносить информацию об атаке зомби - это может оказаться важно на начальном этапе.
- Шанс изолировать инфицированного. Об этом уже говорилось выше.

Чтобы выяснить оптимальные значения этих параметров я прикрутил к модели генетический алгоритм. И вот что получилось:

людей: 100
инфицированных: 10
эпох ГА: 20
количество хромосом: 20
коэффициент мутации: 0.03
запусков: 500

функция фитнеса: отношение количества зомби к количеству людей по завершении работы модели. - т.е. решалась задача минимизации.


результат:
* Общий коэффициент смелости: 1.127
* Шанс объединится в группу: 0.61, коэффициент увеличения: 0.03
* Шанс изолировать инфицированного: 0.94.

Судя по графикам результат не многим лучше чем результат при значениях по умолчанию (которые впрочем, случайно получились, довольно близкими к оптимальным) 1.0, 0.3, 0.03, 0.5 соответственно.

А теперь выводы человеческим языком: люди! если вас больше, не бойтесь. - действуйте решительно. Не спешите сразу объединятся в группы и идти на охоту, бейте тревогу! Обзвоните всех знакомых и предупредите их. И наконец, если человек заражен - немедленно изолируйте его, например заприте где нибудь, или свяжите и привяжите к стулу. От раненного бойца который вот-вот сам станет зомби мало толку.

Надеюсь мои советы окажутся полезны. Помните - они были получены научным путем.






суббота, 24 октября 2009 г.

Программа для резервного копирования / синхронизации данных.

Написал на Steel Bank Common Lisp.

Алгоритм простой:
  • Если файл/директория есть в src но нет в dest - то он/она копируется в dest.
  • Если файл/директория есть в dest но нет в src - то он/она удаляется из dest.
  • Если файл есть в src и в dest, но размеры их не совпадают - то файл из src переписывает файл из dest.
  • Алгоритм вызывается рекурсивно для всех поддиректорий.
Если не удалось скопировать файл/создать директорию - то выводится сообщение об ошибке и алгоритм продолжает работать дальше.

Работает, я считаю, довольно шустро - у меня синхронизация папки 1.4 гб с 53 тыс файлов занимает примерно 10 минут.


;;;; Created on 2009-10-18 13:12:10

(load "F:/Lisp/cl-fad-0.6.3/load.lisp")

(defmacro my-with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))

(defmacro exec-time (executions (&body body))
  (my-with-gensyms (start end time)
    `(progn
       (setf ,start (get-internal-real-time))
       (loop for i from 1 to ,executions do ,body)
       (setf ,end (get-internal-real-time))
       (setf ,time (float (/ (- ,end ,start) internal-time-units-per-second)))
       ,time)
    )
  )

(defun path-to-str (path)
  (format nil "~a" path))

(defun file-len (filepath)
  (with-open-file (in filepath) (file-length in)))

(defun dirs-and-files (path)
  (let* ((files-dirs (cl-fad:list-directory path))
         (dirs (remove-if-not #'cl-fad:directory-pathname-p files-dirs))
         (files (remove-if #'cl-fad:directory-pathname-p files-dirs)))    
    (values dirs files)))

(defun file-name (path)
  (let ((ext (pathname-type path)))
    (concatenate 'string (pathname-name path)
                 (if ext (concatenate 'string "." ext)))
    ))

(defun dir-name (path)
  (car (last (pathname-directory path))))

(defun new-deleted-existed (dest-paths src-paths is-files)
  (let* ((key-func (if is-files #'file-name #'dir-name))
         (new (set-difference src-paths dest-paths
                              :key key-func :test #'string-equal))
         (deleted (set-difference dest-paths src-paths
                              :key key-func :test #'string-equal))
         (existed (intersection src-paths dest-paths
                              :key key-func :test #'string-equal)))
    (values new deleted existed)))

(defun get-relative (base-path full-path)
  (let ((base-path-str (path-to-str base-path))
        (full-path-str (path-to-str full-path)))
    (pathname (subseq full-path-str (length base-path-str)))
    ))

(defun full-dest-path (base-dest-path base-src-path full-src-path)
  (let ((relative (get-relative base-src-path full-src-path)))
    (merge-pathnames relative base-dest-path)
    ))

(defmacro sefe-progn (&body body)
  `(handler-case (progn ,@body) (error (r) (format t "~a~%" r))))

(defun synchronize (dest src)  
  (let ((dest (pathname dest)) (src (pathname src))
        (dirs-src nil) (files-src nil) (dirs-dest nil) (files-dest nil)
        (new-files nil) (deleted-files nil) (existed-files nil)
        (new-dirs nil) (deleted-dirs nil) (existed-dirs nil))
    (setf (values dirs-src files-src) (dirs-and-files src))
    (setf (values dirs-dest files-dest) (dirs-and-files dest))
    (setf (values new-files deleted-files existed-files)
          (new-deleted-existed files-dest files-src T))
    (setf (values new-dirs deleted-dirs existed-dirs)
          (new-deleted-existed dirs-dest dirs-src nil))    
    (loop for new in new-files
          for new-dest = (full-dest-path dest src new)
          do (sefe-progn
               (format t "Copy FROM ~a TO ~a~%" new new-dest)
               (cl-fad:copy-file new new-dest)))  
    (loop for deleted in deleted-files
          do (sefe-progn
               (format t "Delete file ~a~%" deleted)
               (delete-file deleted)))
    (loop for existed in existed-files
          for existed-dest = (full-dest-path dest src existed)
          do (sefe-progn
              (if (/= (file-len existed) (file-len existed-dest))
                 (progn
                   (format t "Overwrite FROM ~a TO ~a~%" existed existed-dest)
                   (cl-fad:copy-file existed existed-dest :overwrite t)))))
    (loop for new in new-dirs
          for new-dest = (full-dest-path dest src new)  
          do (sefe-progn
               (format t "Create directory ~a~%" new-dest)
               (ensure-directories-exist new-dest)
               (synchronize new-dest new)))
    (loop for deleted in deleted-dirs
          do (sefe-progn
               (format t "Delete directory ~a~%" deleted)
               (cl-fad:delete-directory-and-files deleted)))
    (loop for existed in existed-dirs
          for existed-dest = (full-dest-path dest src existed)
          do (synchronize existed-dest existed))
    ))

(defun main (args)
  (let ((src (if (= 3 (length args))
              (nth 1 args) "F:/jFiles/workspaces/tests/file-sync/2"))
        (dest (if (= 3 (length args))
              (nth 2 args) "F:/jFiles/workspaces/tests/file-sync/1")))
    (cl-fad:pathname-as-directory "F:/jFiles/workspaces/tests/file-sync/2")
    (format t "Synchronize ~a ==> ~a~%" src dest)
    (let ((time
          (exec-time 1 (synchronize (cl-fad:pathname-as-directory dest)
                                    (cl-fad:pathname-as-directory src)))))
      (format t "Finished [execution time: ~as]" time))
    ))
     
(main *posix-argv*)
 


Теперь небольшая инструкция, как сделать так, чтобы программа заработала.

Для запуска вам понадобиться SBCL и библиотечка cl-fad которую я использовал для упрощения работы с файлами. В программе указан абсолютный путь к библиотеке "F:/Lisp/cl-fad-0.6.3/load.lisp" - необходимо будет заменить его на путь куда вы установите библиотеку. Сохранив код в файл, например sync.lisp, его можно запускать батником, например таким образом: call sbcl --script sync.lisp "F:/jFiles/" "O:/jFiles/". Возможна одна проблема - при запуске программа будет ругаться что библиотека cl-fad якобы откомпилирована более старой версией SBCL. Не пугайтесь - решается просто, надо пойти в директорию установки cl-fad и удалить файл packages.fasl. Тогда при повторном запуске cl-fad будет перекомпилирована.


Осталось добавить батник в планировщик заданий и радоваться жизни)

воскресенье, 27 сентября 2009 г.

Online Judge unit testing - на LISP-е !!

Уже больше недели изучаю язык Lisp. Неоднократно слышал как его хвалили и решил попробовать действительно ли он так хорош. Изучаю книжке Practical Common Lisp. Вот недавно прочитал главу 9. Practical: Building a Unit Test Framework и решил на основе "фреймворка" реализованного в данной главе построить библиотечку для юнит тестирования в Online Judge.

То, что было реализовано в главе 9 книги назвать фреймворком както язык не поворачивается.. Фреймворк в 16 строчек кода (если не учитывать комментарии) - это смешно) Человеку программирующему на java, C#, C++ трудно понять, что там такого супермощного можно закодировать в 16-ти строчках кода.

Итак взяв за основу эти 16 строк кода я прикрутил проверку времени выполнения, используемой памяти, проверку не было ли выброшено исключение во время работы алгоритма. Вообще по возможностям старался приблизится к решению на C# которое описано в посте Online Judge unit testing.

Получился такой код (кто не знаком с лиспом - пропускайте):

; файл unittesting.lisp
(defvar *test-name* nil)
(defvar *test-time-limit* 2)
(defvar *test-memory-limit* 160)
(defvar *data-files-dir* nil)

(define-condition assert-error (error)
  ((text :initarg :text :reader text)))

(defmacro assert-equal (expected real &optional prefix)
  (with-gensyms (nil e r msg prefValue)
  `(let ((,e ,expected) (,r ,real))
     (if (equal ,e ,r)
     t
     (let* ((,msg (format nil "Expected [~a], but was [~a]" ,e ,r))
            (,prefValue ,prefix))      
         (error 'assert-error :text
           (if ,prefValue (concatenate 'string "[" ,prefValue "] " ,msg) ,msg)))
       ))
    ))

(defmacro time-memory-result ((&body body))
  (with-gensyms (nil start res end time memory)
    `(let* ((,start (get-internal-real-time))
             (,res ,body)
             (,end (get-internal-real-time))
             (,time (float (/ (- ,end ,start) internal-time-units-per-second)))
             (,memory (float (/ (sys::%room) (* 1024 1024)))))
       (list ,time ,memory ,res))
    )
  )

(defmacro deftest (name parameters &body body)
  "Define a test function. Within a test function we can call
   other test functions or use 'check' to run individual test
   cases."

  `(defun ,name ,parameters
    (let ((*test-name* (append *test-name* (list ',name))))
      ,@body)))
 
(defun validate (time memory res)
  (let ((violations nil))
    (progn
      (if (> memory *test-memory-limit*)
        (setf violations (cons
         (format nil "[Memory limit exceeded] Expected less than ~aMB, but was ~aMB"
           *test-memory-limit* memory) violations)))
      (if (> time *test-time-limit*)
        (setf violations (cons
         (format nil "[Time limit exceeded] Expected less than ~as, but was ~as"
           *test-time-limit* time) violations)))
      (if (not res)
        (setf violations (cons "Return NIL" violations)))
      violations)
    ))

(defmacro check (&body forms)
  "Run each expression in 'forms' as a test case."
  (with-gensyms (nil time-memory-res time memory res violations ae e)
  `(combine-results
    ,@(loop for f in forms collect        
        `(handler-case
           (let* ((,time-memory-res (time-memory-result ,f))
                   (,time (nth 0 ,time-memory-res))
                   (,memory (nth 1 ,time-memory-res))
                   (,res (nth 2 ,time-memory-res))
                   (,violations (validate ,time ,memory ,res)))
             (report-result ,violations ',f))
           (assert-error (,ae) (report-result (list (text ,ae)) ',f))
           (error (,e) (report-result
              (list (format nil "Error during execution: ~a" ,e)) ',f)))))
    ))
 
(defmacro combine-results (&body forms)
  "Combine the results (as booleans) of evaluating 'forms' in order."
  (with-gensyms (nil allTestsSuccess)
    `(let ((,allTestsSuccess t))
      ,@(loop for f in forms collect `(unless ,f (setf ,allTestsSuccess nil)))
      ,allTestsSuccess)))
 
(defun report-result (violations form)
  "Report the results of a single test case. Called by 'check'."
  (if (= (length violations) 0)
    (format t "pass ... ~a: ~a~%" *test-name* form)
    (format t "FAIL ... ~a: ~a~%   reason: ~{~A~^, ~} ~%"
      *test-name* form violations))
  (= (length violations) 0))


(defun assert-eq-streams (in1 in2)
  (let ((lineNum 0))
    (progn
      (loop
        for line1 = (read-line in1 nil)
        for line2 = (read-line in2 nil)        
        while (or line1 line2)
          do
         (progn
           (incf lineNum)
           (assert-equal line1 line2 (format nil "line ~a" lineNum))))
      t)
    ))

(defun relativep (path)
  (let ((dir (pathname-directory path)))
    (or (= 0 (length dir)) (equal :relative (nth 0 dir)))
    ))

(defun make-absolute-path (path)
  (if (relativep path) (merge-pathnames path *data-files-dir*) path)
  )

(defun file-io-test (algorithm inPath outPath)
  (progn
    (setf inPath (make-absolute-path inPath))
    (setf outPath (make-absolute-path outPath))
    (let ((res (with-open-file (in inPath
                                 :if-does-not-exist nil)
                 (with-output-to-string (out)
                   (funcall algorithm in out))
                 )))
      (with-input-from-string (result res)
        (with-open-file (expected outPath
                          :if-does-not-exist nil)
          (assert-eq-streams expected result)
          ))
      )
    ))



Как этим пользоваться:

(load "F:\\jFiles\\workspaces\\tests\\SPOJ\\ARITH\\ARITH.lisp") ; загружаем файл с алгоритмом
; алгоритм - реализован в функции exec которая принимает 2 потока - входной и выходной.
(load "F:\\jFiles\\workspaces\\tests\\SPOJ\\unittesting.lisp") ; загружаем библиотеку (код выше)

(setf *test-time-limit* 5) ; устанавливаем ограничение по времени в секундах для каждого теста
(setf *test-memory-limit* 16)  ; устанавливаем ограничение по памяти
(setf *data-files-dir* #p"F:\\jFiles\\workspaces\\tests\\SPOJ\\ARITH\\data\\") ; устанавливаем абсолютный
                                                               ; путь к примерам входных и выходных данных

(deftest tests ()  ; определение тестов (каждая строчка внутри check - один тест)
  (check
    (file-io-test #'exec #p"in1.txt" #p"out1.txt")
    (file-io-test #'exec #p"in2.txt" #p"out2.txt")
    (file-io-test #'exec #p"in3.txt" #p"out3.txt")
    (file-io-test #'exec #p"in4.txt" #p"out4.txt")
    (file-io-test #'exec #p"in5.txt" #p"out5.txt")
    (file-io-test #'exec #p"in6.txt" #p"out6.txt")
    ))

(tests) ; запуск тестов


Пример возможного отчета по тестам:

pass ... (TESTS): (FILE-IO-TEST #'EXEC in1.txt out1.txt)
pass ... (TESTS): (FILE-IO-TEST #'EXEC in2.txt out2.txt)
pass ... (TESTS): (FILE-IO-TEST #'EXEC in3.txt out3.txt)
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in4.txt out4.txt)
reason: [line 18] Expected [ ---------], but was [---------]
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in5.txt out5.txt)
reason:
Error during execution:
WRITE-LINE: keyword arguments in (#) should occur pairwise
FAIL ... (TESTS): (FILE-IO-TEST #'EXEC in6.txt out6.txt)
reason: [Time limit exceeded] Expected less than 5s, but was 6.0670038s, [Memory limit exceeded] Expected less than 16MB, but was 99.79423MB

Первые 3 теста прошли успешно.
Тест 4 свалился по причине того что в строке 18 вывод алгортма перестал совпадать с ожидаемым выводом.
Тест 5 свалился из-за ошибки в рантайме.
Тест 6 провалился по 2 причинам: превышены лимиты времени и памяти.

Итак, всего в 127 строчках кода удалось выразить все то же что и на C# используя суперфрейморк тестирования и вижуал студию для запуска тестов :) Как по мне - хороший результат)

четверг, 17 сентября 2009 г.

Для тех кто работает в связке Eclipse, TortoiseSVN будет полезно

Часто приходится выполнять действие blame для некоторого файла с исходным кодом.

Что для этого нужно? Я в свое время нашел для себя такую последовательность действий:
Итак: исходная точка - файл открыт в редакторе эклипса.
  1. Alt + Enter открываем свойства файла.
  2. Из Location: копируем путь к папке в которой находится файл
  3. Win+E открываем окно виндовс эксплорера
  4. Ctrl+V вставляем путь
  5. Enter - открываем папку
  6. Ищем глазами файл.. (слишком долго..)
  7. Нашли файл -> правой кнопкой -> TortoiseSVN -> Blame (долго)
Аж 7 шагов!!! Слишком долго как для такого простого действия.

Я решил ускорить этот процесс:
  1. Alt + Enter открываем свойства файла.
  2. Из Location: копируем путь к файлу
  3. Win+D чтобы перейти к рабочему столу
  4. клик по blame.bat
Это уже лучше!

Для того чтобы все это работало вам необходимо:
1) Скачать пакет утилит outwit http://www.dmst.aueb.gr/dds/sw/outwit/
Нас там будет интересовать winclip, которая может вытаскивать текст из буфера обмена.
2) Указать в переменной Path путь к outwit-bin-1.25\bin
3) Также в Path должен быть путь к \TortoiseSVN\bin
4) скрипт blame.bat (не пугайтесь; обьяснять не буду; если интересно - сами попробуйте понять):
<nul (set/p z=.exec_blame.bat ) > .exec_buffer.bat
winclip -p >> .exec_buffer.bat
call .exec_buffer.bat
5) в той же директории где находится blame.bat создайте вспомогательный .exec_blame.bat. Его содержание:
TortoiseProc /command:blame /path:%1

Это все)

суббота, 29 августа 2009 г.

Что делает функцию разделения строки split в джаве такой "уникальной"

Казалось бы, функция split разделения строки на части по заданному разделителю, что в ней может быть такого особенного. Ну делит и делит себе, все отлично..

Посмотрим на пример написанный на Python:

s = ',1,,2,,'
tokens = s.split(',')
print('start')
for token in tokens:
print(token)
print('end')
_Winnie Colorizer


Выполняем.. вывод:
start

1

2


end

Все четко и предсказуемо. На стыке разделителей считается что находится пустая строка.

Теперь посмотрим пример на C#:

String[] tokens = ",1,,2,,".Split(new []{','});
Console.WriteLine("start");
for (int i = 0; i < tokens.Length; ++i )
{
Console.WriteLine(tokens[i]);
}
Console.WriteLine("end");
_Winnie Colorizer


Вывод аналогичен:
start

1

2


end

Ок, значит если я сейчас напишу код на java

String[] tokens = ",1,,2,,".split(",");
System.out.println("start");
for (int i = 0; i < tokens.length; ++i)
{
System.out.println(tokens[i]);
}
System.out.println("end");
_Winnie Colorizer


то можно с полной уверенность ожидать что вывод будет точно таким же. Ага.. Как же - черта с два!

start

1

2
end

Вот таким образом. Стоящие подряд разделители в конце строки почему-то полностью игнорируются.
Джавадок говорит следующее:
This method works as if by invoking the two-argument split method with the given expression and a limit argument of zero. Trailing empty strings are therefore not included in the resulting array.

Опять не понятно - с чего вдруг "empty strings are therefore not included in the resulting array". Ситуацию проясняет только джавадок к методу:
public String[] split(String regex, int limit)

...
The limit parameter controls the number of times the pattern is applied and therefore affects the length of the resulting array. If the limit n is greater than zero then the pattern will be applied at most n - 1 times, the array's length will be no greater than n, and the array's last entry will contain all input beyond the last matched delimiter. If n is non-positive then the pattern will be applied as many times as possible and the array can have any length. If n is zero then the pattern will be applied as many times as possible, the array can have any length, and trailing empty strings will be discarded.
...

Как жаль. А мне было сначала показалось что я нашел багу.. Оказалось просто такая вот дурацкая особенность, которую надо иметь в виду.

понедельник, 24 августа 2009 г.

Коварный Hibernate или проблемма @OneToOne(targetEntity = SomeAbstractEntityClass, fetch = FetchType.LAZY)

Как известно хибернет умеет загружать сущности по асоциации OneToOne ленивым образом.
Для этого нужно указать в аннотации @OneToOne что fetch = FetchType.LAZY. Что это дает?
А то что очень часто когда мы достаем некоторую сущность из базы, нам не нужны сущности прилинкованные к ней. Тогда и вытаскивать их из базы нет смысла. Вот как раз FetchType.LAZY дает то, что прилинкованная по OneToOne сущность подгружается только тогда когда мы начинаем обращаться к полям этой сущности. Полезная штука.. НО! Совсем недавно натолкнулся на совершенно невероятную на первый взгляд вещь..

Покажу на примере.
Пусть у нас имеются такие сущности:

AbstractWeapon implements IWeapon - абстрактное оружие.
Sword extends AbstractWeapon implements ISword - меч.
Bow extends AbstractWeapon implements IBow - лук.
Интерфейсы ISward и IBow наследуются от IWeapon.

Warrior - воин. Который владеет неким "абстрактным" оружием:
@OneToOne(targetEntity = AbstractWeapon.class, fetch = FetchType.LAZY)
@JoinColumn(name = "wr_wp_aa")
public IWeapon getWeapon()
.....

И предположим что у воина есть метод, который по типу оружия определяет к какому классу отнести воина.
Если это меч то - мечник, если лук то соответственно - лучник.

   public WarriorType getType()
{
WarriorType type;

if (weapon instanceof ISword)
{
type = WarriorType.SWORDSMAN;
}
else if (weapon instanceof IBow)
{
type = WarriorType.ARCHER;
}
else
{
throw new IllegalStateException("incorrect type of weapon: " + weapon);
}

return type;
}
_Winnie Colorizer


Теперь сохраняем в базу данных одно оружие - меч, и одного воина - который владеет этоим мечем. А теперь внимание вопрос: что будет если сейчас вытащить этого воина средствами хибернета, и попробовать узнать его тип, вызвав метод getType?

Вы было подумали что метод вернет WarriorType.SWORDSMAN? Увы, вынужден вас огорчить, но вы жестоко ошиблись.. Метод кинет исключение IllegalStateException.

Начинаем дебажить и выясняем всю правду:
weapon на самом деле не Sword, а обьект некого таинственного типа AbstractWeapon_$_javassist_1. Смотрим внимательнее, и видим что дерево наследования у AbstractWeapon_$_javassist_1 имеет примерно такой вид:

com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon_$$_javassist_1
extends
com.greentea.relaxation.serverplays.shp.business.weapons.AbstractWeapon
extends
com.greentea.relaxation.serverplays.shp.business.AbstractEntity
implements
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
com.greentea.relaxation.serverplays.shp.business.weapons.IWeapon
extends
com.greentea.relaxation.serverplays.shp.business.IEntity
implements
org.hibernate.proxy.HibernateProxy
extends
java.io.Serializable
javassist.util.proxy.ProxyObject

Нигде! Как вы видите - нигде, нету в этом дереве типа Sword. Итак, выясняется что weapon это прокси
(реализует интерфейсы org.hibernate.proxy.HibernateProxy и javassist.util.proxy.ProxyObject) для
класса AbstractWeapon. И никаким образом в рантайме честно узнать подтип по instanceof или привести к одному из типов производных от AbstractWeapon не получится..

Существуют конечно обходные пути, например:
- Метод Hibernate.getClass - вернет тип Sword.
- Сам пример немножко надуманный, и можно было бы реализовать getType по другому.
- Также можно вообще радикально решить проблемму отказавшись от LAZY.

Мне кажется что Hibernate делает прокси не совсем правильно. По логике, он даже в случае с LAZY мог бы вначале вытащить DiscriminatorValue из таблицы, определить тип, и сделать свое прокси уже от реального производного типа, т.е. от Sword, а не от абстрактного AbstractWeapon. Но вероятно разработчики Hibernate посчитали что если LAZY то LAZY до конца, и один дополнительный запрос - это будет слишком расточительно. Иначе как это еще можно обьяснить.

Постоянные читатели